From 79cc362c22545b987e11cc98e5a3e3b9f10e4ef9 Mon Sep 17 00:00:00 2001 From: Jonathan Ho Date: Wed, 6 Dec 2023 17:00:20 -0800 Subject: [PATCH] feat: add site back to website (temp) (#3267) * feat: add site back to website * refactor: use same sidebar * fix: delete generated docs --- website/docusaurus.config.js | 74 ++ .../old_docs/amethyst/AmethystCollection.md | 7 + website/old_docs/amethyst/AmethystEmbed.md | 7 + website/old_docs/amethyst/_category_.json | 1 + website/old_docs/amethyst/client.md | 99 ++ website/old_docs/amethyst/exampleBot.md | 82 ++ website/old_docs/amethyst/intro.md | 54 + website/old_docs/architecture.mdx | 49 + website/old_docs/benchmark.mdx | 12 + .../old_docs/big-bot-guide/_category_.json | 4 + website/old_docs/big-bot-guide/cache.md | 119 ++ website/old_docs/big-bot-guide/events.md | 261 ++++ website/old_docs/big-bot-guide/gateway.md | 1076 +++++++++++++++++ website/old_docs/big-bot-guide/rest.md | 134 ++ .../old_docs/big-bot-guide/step-by-step.md | 40 + .../old_docs/frequently-asked-questions.md | 107 ++ .../general/frequently-asked-questions.md | 107 ++ website/old_docs/general/getting-started.md | 80 ++ website/old_docs/general/migrating.md | 487 ++++++++ website/old_docs/getting-started.md | 80 ++ website/old_docs/intro.md | 23 + website/old_docs/migrating.md | 487 ++++++++ .../nodejs/CommandHandler/_category_.json | 4 + .../nodejs/CommandHandler/command-manager.md | 92 ++ .../nodejs/CommandHandler/create-command.md | 61 + .../nodejs/CommandHandler/getting-started.md | 22 + .../nodejs/EventHandler/_category_.json | 4 + .../nodejs/EventHandler/event-manager.md | 123 ++ .../nodejs/EventHandler/getting-started.md | 29 + .../nodejs/EventHandler/handle-event.md | 76 ++ .../nodejs/Structures/_category_.json | 4 + .../old_docs/nodejs/Structures/collectors.md | 56 + .../old_docs/nodejs/Structures/components.md | 229 ++++ .../nodejs/Structures/create-structure.md | 100 ++ website/old_docs/nodejs/Structures/embeds.md | 112 ++ .../nodejs/Structures/getting-started.md | 30 + website/old_docs/nodejs/_category_.json | 4 + website/old_docs/nodejs/create-application.md | 29 + website/old_docs/nodejs/design.md | 203 ++++ website/old_docs/nodejs/getting-started.md | 48 + website/old_docs/nodejs/initial-setup.md | 48 + website/old_docs/nodejs/installion.md | 30 + website/old_docs/nodejs/slash-command.md | 70 ++ website/package.json | 1 + website/sidebars.js | 3 +- .../components/architecture/BaseFlowChart.tsx | 125 ++ .../src/components/architecture/FlowChart.tsx | 165 +++ .../components/architecture/FlowChart2.tsx | 310 +++++ .../components/architecture/FlowChart3.tsx | 910 ++++++++++++++ .../tutorial/amethyst/AmethystCollection.md | 7 + website/tutorial/amethyst/AmethystEmbed.md | 7 + website/tutorial/amethyst/_category_.json | 1 + website/tutorial/amethyst/client.md | 99 ++ website/tutorial/amethyst/exampleBot.md | 82 ++ website/tutorial/amethyst/intro.md | 54 + .../tutorial/big-bot-guide/_category_.json | 4 + website/tutorial/big-bot-guide/cache.md | 119 ++ website/tutorial/big-bot-guide/events.md | 261 ++++ website/tutorial/big-bot-guide/gateway.md | 1076 +++++++++++++++++ website/tutorial/big-bot-guide/rest.md | 134 ++ .../tutorial/big-bot-guide/step-by-step.md | 40 + .../nodejs/CommandHandler/_category_.json | 4 + .../nodejs/CommandHandler/command-manager.md | 92 ++ .../nodejs/CommandHandler/create-command.md | 61 + .../nodejs/CommandHandler/getting-started.md | 22 + .../nodejs/EventHandler/_category_.json | 4 + .../nodejs/EventHandler/event-manager.md | 123 ++ .../nodejs/EventHandler/getting-started.md | 29 + .../nodejs/EventHandler/handle-event.md | 76 ++ .../nodejs/Structures/_category_.json | 4 + .../tutorial/nodejs/Structures/collectors.md | 56 + .../tutorial/nodejs/Structures/components.md | 229 ++++ .../nodejs/Structures/create-structure.md | 100 ++ website/tutorial/nodejs/Structures/embeds.md | 112 ++ .../nodejs/Structures/getting-started.md | 30 + website/tutorial/nodejs/_category_.json | 4 + website/tutorial/nodejs/create-application.md | 29 + website/tutorial/nodejs/design.md | 203 ++++ website/tutorial/nodejs/getting-started.md | 48 + website/tutorial/nodejs/initial-setup.md | 48 + website/tutorial/nodejs/installion.md | 30 + website/tutorial/nodejs/slash-command.md | 70 ++ website/yarn.lock | 511 ++++++++ 83 files changed, 10046 insertions(+), 1 deletion(-) create mode 100644 website/old_docs/amethyst/AmethystCollection.md create mode 100644 website/old_docs/amethyst/AmethystEmbed.md create mode 100644 website/old_docs/amethyst/_category_.json create mode 100644 website/old_docs/amethyst/client.md create mode 100644 website/old_docs/amethyst/exampleBot.md create mode 100644 website/old_docs/amethyst/intro.md create mode 100644 website/old_docs/architecture.mdx create mode 100644 website/old_docs/benchmark.mdx create mode 100644 website/old_docs/big-bot-guide/_category_.json create mode 100644 website/old_docs/big-bot-guide/cache.md create mode 100644 website/old_docs/big-bot-guide/events.md create mode 100644 website/old_docs/big-bot-guide/gateway.md create mode 100644 website/old_docs/big-bot-guide/rest.md create mode 100644 website/old_docs/big-bot-guide/step-by-step.md create mode 100644 website/old_docs/frequently-asked-questions.md create mode 100644 website/old_docs/general/frequently-asked-questions.md create mode 100644 website/old_docs/general/getting-started.md create mode 100644 website/old_docs/general/migrating.md create mode 100644 website/old_docs/getting-started.md create mode 100644 website/old_docs/intro.md create mode 100644 website/old_docs/migrating.md create mode 100644 website/old_docs/nodejs/CommandHandler/_category_.json create mode 100644 website/old_docs/nodejs/CommandHandler/command-manager.md create mode 100644 website/old_docs/nodejs/CommandHandler/create-command.md create mode 100644 website/old_docs/nodejs/CommandHandler/getting-started.md create mode 100644 website/old_docs/nodejs/EventHandler/_category_.json create mode 100644 website/old_docs/nodejs/EventHandler/event-manager.md create mode 100644 website/old_docs/nodejs/EventHandler/getting-started.md create mode 100644 website/old_docs/nodejs/EventHandler/handle-event.md create mode 100644 website/old_docs/nodejs/Structures/_category_.json create mode 100644 website/old_docs/nodejs/Structures/collectors.md create mode 100644 website/old_docs/nodejs/Structures/components.md create mode 100644 website/old_docs/nodejs/Structures/create-structure.md create mode 100644 website/old_docs/nodejs/Structures/embeds.md create mode 100644 website/old_docs/nodejs/Structures/getting-started.md create mode 100644 website/old_docs/nodejs/_category_.json create mode 100644 website/old_docs/nodejs/create-application.md create mode 100644 website/old_docs/nodejs/design.md create mode 100644 website/old_docs/nodejs/getting-started.md create mode 100644 website/old_docs/nodejs/initial-setup.md create mode 100644 website/old_docs/nodejs/installion.md create mode 100644 website/old_docs/nodejs/slash-command.md create mode 100644 website/src/components/architecture/BaseFlowChart.tsx create mode 100644 website/src/components/architecture/FlowChart.tsx create mode 100644 website/src/components/architecture/FlowChart2.tsx create mode 100644 website/src/components/architecture/FlowChart3.tsx create mode 100644 website/tutorial/amethyst/AmethystCollection.md create mode 100644 website/tutorial/amethyst/AmethystEmbed.md create mode 100644 website/tutorial/amethyst/_category_.json create mode 100644 website/tutorial/amethyst/client.md create mode 100644 website/tutorial/amethyst/exampleBot.md create mode 100644 website/tutorial/amethyst/intro.md create mode 100644 website/tutorial/big-bot-guide/_category_.json create mode 100644 website/tutorial/big-bot-guide/cache.md create mode 100644 website/tutorial/big-bot-guide/events.md create mode 100644 website/tutorial/big-bot-guide/gateway.md create mode 100644 website/tutorial/big-bot-guide/rest.md create mode 100644 website/tutorial/big-bot-guide/step-by-step.md create mode 100644 website/tutorial/nodejs/CommandHandler/_category_.json create mode 100644 website/tutorial/nodejs/CommandHandler/command-manager.md create mode 100644 website/tutorial/nodejs/CommandHandler/create-command.md create mode 100644 website/tutorial/nodejs/CommandHandler/getting-started.md create mode 100644 website/tutorial/nodejs/EventHandler/_category_.json create mode 100644 website/tutorial/nodejs/EventHandler/event-manager.md create mode 100644 website/tutorial/nodejs/EventHandler/getting-started.md create mode 100644 website/tutorial/nodejs/EventHandler/handle-event.md create mode 100644 website/tutorial/nodejs/Structures/_category_.json create mode 100644 website/tutorial/nodejs/Structures/collectors.md create mode 100644 website/tutorial/nodejs/Structures/components.md create mode 100644 website/tutorial/nodejs/Structures/create-structure.md create mode 100644 website/tutorial/nodejs/Structures/embeds.md create mode 100644 website/tutorial/nodejs/Structures/getting-started.md create mode 100644 website/tutorial/nodejs/_category_.json create mode 100644 website/tutorial/nodejs/create-application.md create mode 100644 website/tutorial/nodejs/design.md create mode 100644 website/tutorial/nodejs/getting-started.md create mode 100644 website/tutorial/nodejs/initial-setup.md create mode 100644 website/tutorial/nodejs/installion.md create mode 100644 website/tutorial/nodejs/slash-command.md diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index ce4118e96..9d75a4bf8 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -84,6 +84,20 @@ const config = { position: 'left', label: 'Documentation', }, + { + type: 'docSidebar', + sidebarId: 'old_docs', + position: 'left', + label: 'Old Docs', + docsPluginId: 'old_docs', + }, + { + type: 'docSidebar', + sidebarId: 'tutorial', + position: 'left', + label: 'Tutorial', + docsPluginId: 'tutorial', + }, { to: '/blog', label: 'Blog', position: 'left' }, { href: 'https://discord.gg/ddeno', @@ -109,6 +123,44 @@ const config = { }, ], }, + { + title: 'Old Docs', + items: [ + { + label: 'Introduction', + to: '/old_docs/intro', + }, + { + label: 'Getting Started', + to: '/old_docs/getting-started', + }, + { + label: 'FAQ', + to: '/old_docs/frequently-asked-questions', + }, + { + label: 'Benchmark', + to: '/old_docs/benchmark', + }, + ], + }, + { + title: 'Tutorial', + items: [ + { + label: 'Big Bot', + to: '/tutorial/big-bot-guide/step-by-step', + }, + { + label: 'Node.js', + to: '/tutorial/nodejs/getting-started', + }, + { + label: 'Amethyst', + to: '/tutorial/amethyst/intro', + }, + ], + }, { title: 'Community', items: [ @@ -141,6 +193,28 @@ const config = { }), plugins: [ + [ + '@docusaurus/plugin-content-docs', + /** @type {import('@docusaurus/plugin-content-docs').Options} */ + { + id: 'old_docs', + path: 'old_docs', + routeBasePath: 'old_docs', + sidebarPath: require.resolve('./sidebars.js'), + editUrl: 'https://github.com/discordeno/discordeno/tree/main/site/', + }, + ], + [ + '@docusaurus/plugin-content-docs', + /** @type {import('@docusaurus/plugin-content-docs').Options} */ + { + id: 'tutorial', + path: 'tutorial', + routeBasePath: 'tutorial', + sidebarPath: require.resolve('./sidebars.js'), + editUrl: 'https://github.com/discordeno/discordeno/tree/main/site/', + }, + ], [ require.resolve('@easyops-cn/docusaurus-search-local'), { diff --git a/website/old_docs/amethyst/AmethystCollection.md b/website/old_docs/amethyst/AmethystCollection.md new file mode 100644 index 000000000..14e0ddf68 --- /dev/null +++ b/website/old_docs/amethyst/AmethystCollection.md @@ -0,0 +1,7 @@ +--- +sidebar_position: 5 +--- + +# Same as discord.js [collection][def] + +[def]: https://discord.js.org/#/docs/collection/main/class/Collection diff --git a/website/old_docs/amethyst/AmethystEmbed.md b/website/old_docs/amethyst/AmethystEmbed.md new file mode 100644 index 000000000..f654b38d6 --- /dev/null +++ b/website/old_docs/amethyst/AmethystEmbed.md @@ -0,0 +1,7 @@ +--- +sidebar_position: 4 +--- + +# Documentation [at][def] + +[def]: https://deno.land/x/amethyst@v4.3.4/mod.ts?s=AmethystEmbed diff --git a/website/old_docs/amethyst/_category_.json b/website/old_docs/amethyst/_category_.json new file mode 100644 index 000000000..b564ff089 --- /dev/null +++ b/website/old_docs/amethyst/_category_.json @@ -0,0 +1 @@ +{ "label": "Amethyst Framework", "position": 4 } diff --git a/website/old_docs/amethyst/client.md b/website/old_docs/amethyst/client.md new file mode 100644 index 000000000..cc2fba13d --- /dev/null +++ b/website/old_docs/amethyst/client.md @@ -0,0 +1,99 @@ +--- +sidebar_position: 2 +--- + +# Creating an client + +Let's review each choice and what it does. + +- `owners`, You may specify the proprietors of the bot using this. The inhibitors make use of this. +- `prefix`, The string a user should use at the beginning of their message to identify it as a command to the bot. Only + message commands can use this, and the parameter can be either a string or a function. +- `botMentionAsPrefix`, Determines whether a user's mention of a bot qualifies as a prefix. +- `ignoreBots`, Allow bots to execute commands. +- `defaultCooldown`, Defualt cooldown for all commands. +- `ignoreCooldown`, List of people who bypass cooldowns. +- `commandDir`, Path to the command directory used by the fileloader. +- `eventDir`, Path to the event directory used by the fileloader. +- `inhibitorDir`, Path to the inhibitor directory used by the fileloader. +- `prefixCaseSensitive`, Indicates whether or not the prefix is case-sensitive. +- `extras`, Extras that are used by your client, such as a database instance or a music player. + +## Client Extras + +When using discord.js we often do stuff like `client.musicplayer=player;` and in order to maintain this ease Amethyst +allows you to do `client.extras.musicplayer=player;`. + +NOTE: Typing will not work on `client.extras`. + +## Client Properties + +```ts + user: User; + events: AmethystEvents; + messageCollectors: AmethystCollection; + componentCollectors: AmethystCollection; + reactionCollectors: AmethystCollection; + runningTasks: runningTasks; + tasks: AmethystCollection; + category: AmethystCollection; + inhibitors: AmethystCollection< + string, + ( + bot: AmethystBot, + command: T, + options: { memberId?: bigint; channelId: bigint; guildId?: bigint } + ) => true | AmethystError + >; + owners?: bigint[]; + botMentionAsPrefix?: boolean; + prefixCaseSensitive?: boolean; + defaultCooldown?: CommandCooldown; + ignoreCooldown?: bigint[]; + guildOnly?: boolean; + messageQuotedArguments?: boolean; + ignoreBots?: boolean; + dmOnly?: boolean; + eventHandler: AmethystEventHandler; + extras: any; + prefix?: + | string + | string[] + | ((bot: AmethystBot, message: Message) => Async); + + on(name: string, callback: (...args: any) => unknown): void; + once(name: string, callback: (...args: any) => unknown): void; + + amethystUtils: { + awaitComponent( + messageId: bigint, + options?: ComponentCollectorOptions & { maxUsage?: number } + ): Promise, + awaitReaction( + messageId: bigint, + options?: ReactionCollectorOptions & { maxUsage?: number } + ): Promise, + awaitMessage( + memberId: bigint, + channelId: bigint, + options?: MessageCollectorOptions & { maxUsage?: number } + ): Promise, + createCommand(command: CommandOptions): void, + createCategory(category: CategoryOptions): void, + updateCategory(category: CategoryOptions): void, + createTask(task: AmethystTask): void, + clearTasks(): void, + createInhibitor( + name: string, + inhibitor: ( + bot: AmethystBot, + command: T, + options?: { memberId?: bigint; guildId?: bigint; channelId: bigint } + ) => true | AmethystError + ): void, + deleteInhibitor(name: string): void, + updateSlashCommands(): void, + } +``` + +## [Documentation](https://deno.land/x/amethyst@v4.2.0/mod.ts?s=AmethystBotOptions) diff --git a/website/old_docs/amethyst/exampleBot.md b/website/old_docs/amethyst/exampleBot.md new file mode 100644 index 000000000..92b036226 --- /dev/null +++ b/website/old_docs/amethyst/exampleBot.md @@ -0,0 +1,82 @@ +--- +sidebar_position: 3 +--- + +# Lets Create a simple bot in Node.js + +- **Step 1**: Create a typescript project with index.ts as main file. + +- **Step 2**: Installing packages. Install following packages. + +```bash +npm i @thereallonewolf/amethystframework +``` + +- **Step 3**: Create a index.ts file. + +- **Step 4**: Add following code in index.ts file, replacing TOKEN with your bot token. + +```ts +import { createBot, GatewayIntents, startBot } from 'discordeno' +import { enableCachePlugin, enableCacheSweepers } from 'discordeno/cache-plugin' +import { + AmethystBot, + Category, + Command, + Context, + enableAmethystPlugin, + Event, +} from '@thereallonewolf/amethystframework' + +let baseClient = createBot({ + token: 'TOKEN', + intents: + GatewayIntents.Guilds | + GatewayIntents.GuildMessages | + GatewayIntents.MessageContent, +}) + +//@ts-ignore +let client = enableAmethystPlugin(enableCachePlugin(baseClient), { + botMentionAsPrefix: true, + prefix: '!', //Can be a function or a string. + ignoreBots: false, +}) +enableCacheSweepers(client) + +startBot(client) + +@Category({ + name: 'general', + description: 'My general commands', + uniqueCommands: true, + default: '', //As all the commands are unique so no need to set the default command. +}) +export class General { + @Command({ + name: 'ping', + description: 'Pong!', + commandType: ['application', 'message'], + category: 'general', + args: [], + }) + async ping(bot: AmethystBot, ctx: Context) { + ctx.reply({ content: 'Pong!' }) + } + + @Event('ready') + async ready() { + console.log('I am ready!') + client.amethystUtils.updateSlashCommands() + } +} +``` + +- **Step 5**: Invite your bot and compile index.ts and run it. Then you can use `/general ping` or `!ping` + +- **Step 6**: Useful links: + +1. Command Options can be found + [here](https://github.com/AmethystFramework/framework/blob/master/src/types/commandOptions.ts). +2. Category Options [here](https://github.com/AmethystFramework/framework/blob/master/src/types/categoryOptions.ts) +3. Full [Documentation](https://deno.land/x/amethyst) diff --git a/website/old_docs/amethyst/intro.md b/website/old_docs/amethyst/intro.md new file mode 100644 index 000000000..174d5e428 --- /dev/null +++ b/website/old_docs/amethyst/intro.md @@ -0,0 +1,54 @@ +--- +sidebar_position: 1 +--- + +# Amethyst + +Amethyst is a [Discordeno](https://github.com/discordeno/discordeno) plugin that is incredibly robust and flexible. It +promotes standard practices and is geared at bigger bots. + +This framework is not for you if you cannot utilise Maps and Sets without reading them up. We presume you have a solid +foundation in typescript/javascript and dicord bots. + +[Documentation](https://deno.land/x/amethyst) + +## Features + +- Thanks to Amethyst's adaptability, you can change a lot of things and add features as you see appropriate. +- A developer may create slash or message interactions with Amethyst. +- Assistance with interactions, such as selection, built-in buttons, and more. +- Custom discord.js like event system without the event emitter. +- Explore more incredible features of our framework. + +## Why Amethyst? + +Amethyst makes use of the [Discordeno](https://github.com/discordeno/discordeno) plugin system to streamline your coding +process and help you get going more quickly. Support for message and slash commands that doesn't need altering any code. + +- Fully programmable + +- Easy to learn and utilise. + +- Conversion from Discord.js bots is simple. + +## Ideas + +The objective is to provide a framework that can be used with both Node.js and Deno while minimising transitions and +rewrites. + +- Easy to understand and use. +- Cover up complexity in the engine keeping the end user interface as simple as possible. + +## Future Updates + +Creation of discord setup wizards for commands like welcome, context menus and paginated messages. + +## Installation + +Deno: [link](https://deno.land/x/amethyst) + +Npm: + +```bash +npm i @thereallonewolf/amethystframework +``` diff --git a/website/old_docs/architecture.mdx b/website/old_docs/architecture.mdx new file mode 100644 index 000000000..e3745e4c9 --- /dev/null +++ b/website/old_docs/architecture.mdx @@ -0,0 +1,49 @@ +--- +sidebar_position: 2 +--- +import BrowserOnly from '@docusaurus/BrowserOnly'; + +# Architecture +## Overview + +import FlowChart from '@site/src/components/architecture/FlowChart' + + + {() => } + + +Discordeno have three main components/process, gateway, bot and rest. Websocket events from Discord, such as connecting, restarting, heartbeating, and transmitting websocket messages to Discord, are handled and maintained by the Gateway process. All Discord events are handled and converted by the Bot process, which also activates your code, such as the execute function upon message creation. All http requests to Discord, including proxying and ratelimiting, are handled by the Rest process. + +## Gateway Process +import FlowChart2 from '@site/src/components/architecture/FlowChart2' + + + {() => } + + +The Gateway process have two part the gateway manager and the gateway [shard](https://discord.com/developers/docs/topics/gateway#sharding), the gateway manager oversees the gateway shard. + +### Gateway Manager + +The gateway manager spawns the right amount of shard acording to data from discord's [getGatewayBot](https://discord.com/developers/docs/topics/gateway#get-gateway-bot) endpoint, user can override the value of gatewayBot by directly passing the value. The manager control the order or sequence of shard identifying base on the session start limit listed in the getGatewayBot to prevent hitting the ratelimiting. By default the manager will check the getGatewayBot endpoint every 8 hours and reshard if the number of shard changed shard. Check [here](/tutorial/big-bot-guide/gateway#understanding-gateway-manager) for more information about the gateway manager. + +### Gateway Shard + +Any event is passed to the handleMessage method by the gateway shard, which also establishes a websocket connection to Discord. The handleMessage method will examine the event and only deliver genuine events to the bot by intercepting and processing websocket-related events like hello, resume, heartbeat, and ready. You can modify the handleMessage to suit your needs, but it is not advised unless you are certain of your actions because the connection depends on it. After the function has finished processing the event, it will either pass the event directly (in the same process), via the rest api, a message queue, or another mechanism depending on the user's customization. + +## Bot Process +import FlowChart3 from '@site/src/components/architecture/FlowChart3' + + + {() => handler.startsWith('handleChannel')}/>} + + +A simplified version of function used inside of the bot process, showing only the handlers, transformers and event related to channel event + +### Bot + +When an event arrives from the gateway, the bot process receives it and passes it all to the handleDiscordPayload method. The handleDiscordPayload method will invoke raw events for each event and route calls to handlers, transformers, and events at three different layers for processing. The handler will use transformers to alter the event's contents before calling the event. The data will be transformed into typescript after being stripped of pointless properties, given a new name, and having bitwise permission flags abstracted. + +## Rest Process + +TBC \ No newline at end of file diff --git a/website/old_docs/benchmark.mdx b/website/old_docs/benchmark.mdx new file mode 100644 index 000000000..72f7484aa --- /dev/null +++ b/website/old_docs/benchmark.mdx @@ -0,0 +1,12 @@ +--- +sidebar_position: 10 +--- + +# Benchmark + +Benchmark runs on every commit pushed on the Discordeno's main branch + +import Benchmark from '@site/src/components/Benchmark' + + + diff --git a/website/old_docs/big-bot-guide/_category_.json b/website/old_docs/big-bot-guide/_category_.json new file mode 100644 index 000000000..3ff52b832 --- /dev/null +++ b/website/old_docs/big-bot-guide/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Big Bot Guide", + "position": 2 +} diff --git a/website/old_docs/big-bot-guide/cache.md b/website/old_docs/big-bot-guide/cache.md new file mode 100644 index 000000000..18302f8c0 --- /dev/null +++ b/website/old_docs/big-bot-guide/cache.md @@ -0,0 +1,119 @@ +--- +sidebar_position: 4 +sidebar_label: Step 3 - Cache +--- + +# Step 3: Standalone Cache Process + +The next part of this is going to be about making a standalone cache process. By now, you should have both a REST and a +Gateway process ready. Before, we start handling events we should build a Cache handler. + +## Why Use Standalone Cache Process? + +A standalone cache process allows you to retain cached data even after bot restarts. For example, if you are caching +member roles to track when a role was added or removed, you may want to cache the members. The question then comes to +play, when deciding where to keep your cache. Another reason to use this is, whether or not you are using a standalone +gateway process. + +- Start rest process +- Start event handler process (bot) +- Start gateway process. + - Guild create events arrive providing all the data needed to cache in the bot process. +- Restart event handler process(maybe for an update/reboot) + - You lost all guilds/channels/permissions etc and can not get them again without restarting gateway. This defeats the + entire point of the standalone gateway. + +If your cache is tied to the bot processes which is not tied to the gateway you lose all this info. The next thought is +to just keep the Cache entirely in the gateway process however, I do not like this personally however, should you desire +this you can do this as well. The reason I prefer not to do this is when your bot needs to make requests to your cache, +you do not want it occupying the thread for processing other gateway events arriving from discord. A separate cache +process makes it so it uses an entirely separate thread and will not slow down anything else. + +## Understand Cache Types + +When I use the term cache process, this is interchangeable with any similar term such as "custom cache", "redis cache", +"pgsql cache", etc... The fact is you can keep this "cache" anywhere. For this guide, we will implement a very simple +cache using pgsql. Feel free to modify this any way you like as advanced as you like. The point is Discordeno cache is +flexible enough to let you use anything for your Cache storage. + +## Setting Up The Cache + +This step is for you to create the base schema for your cache. For example, if you want to implement a pgsql or redis +cache perhaps you want to prepare the tables/schema. For this guide, we are just going to do a quick little hack to get +a custom cache working. + +Create a file in a path like `src/bot/cache/schema.sql` + +```sql +CREATE TABLE IF NOT EXISTS "users" ( + id bigint NOT NULL, + username text COLLATE pg_catalog."default" NOT NULL, + discriminator text COLLATE pg_catalog."default" NOT NULL, + bot boolean, + CONSTRAINT "users_pkey" PRIMARY KEY (id) +) +``` + +Note that you can cache only properties you want and leave all other properties that you won't use. + +Now that we have this schema ready for our users cache. Go ahead and repeat this for all other cache tables. + +Cache Tables: + +- users +- members +- guilds +- channels +- threads +- messages +- presences +- unavailableGuilds + +Once you are finished continue forward, for the purpose of keeping this guide short we wont cover each table. + +> You should also run this file to prepare your pgsql and have your pgsql database running by now. Or whatever, cache +> service you use. + +### Cache Handler + +Now we will initiate our cache service. This may be different for you based on your choice of cache type. Since we are +using PGSQL for our cache layer, we will now instantiate it. + +```ts +import { postgres } from '../../../deps.ts' + +// YOU CUSTOM PGSQL INFO GOES HERE +const DATABASE_USERNAME = '' +const DATABASE_PASSWORD = '' +const DATABASE_NAME = '' +const DATABASE_HOST = '' +const DATABASE_PORT = 8956 +const DATABASE_MAX = 20 + +export const psql = postgres({ + username: DATABASE_USERNAME, + password: DATABASE_PASSWORD, + database: DATABASE_NAME, + host: DATABASE_HOST, + port: DATABASE_PORT, + max: DATABASE_MAX, + /*onnotice: (data) => { + logger.psql(`${data.severity} ${bgBrightBlack(`[${data.code}| ${data.file}:${data.line}]`)}`, data.message); + },*/ + types: { + bigint: postgres.BigInt, + }, +}) +``` + +To use the PGSQL driver we are using in this guide you can insert this into your `deps.ts`. + +```ts +// @deno-types="https://denopkg.com/porsager/postgres@e2a8595d7aa8c3c838b83b9bca7b890c1707ad2c/types/index.d.ts" +export { default as postgres } from 'https://denopkg.com/porsager/postgres@e2a8595d7aa8c3c838b83b9bca7b890c1707ad2c/deno/lib/index.js' +``` + +> Note: Remember you can use any driver you like. For deno users we prefer to use this library for PGSQL because it is +> more stable and more performant. + +Now that the cache layer is ready, we can proceed to begin creating our bot. diff --git a/website/old_docs/big-bot-guide/events.md b/website/old_docs/big-bot-guide/events.md new file mode 100644 index 000000000..e6f78aa1b --- /dev/null +++ b/website/old_docs/big-bot-guide/events.md @@ -0,0 +1,261 @@ +--- +sidebar_position: 5 +sidebar_label: Step 4 - Event Handler +--- + +# Step 4: Creating Standalone Event Handler + +Now we are about to start working on the bot code itself. The last 3 steps should be completed by the time you reach +this. The event handler process will be listening for events from any number of gateway instances and be ready to handle +them. + +In this guide, we may use the term `Bot` or the term `event handler`, remember that these refer to the same thing. This +is your main bot code. + +## Why Use Standalone Event Handler Process? + +The standalone event handler is the portion of your bot code that you will be changing the most. The three previous +steps created processes that are intended to never be turned off. This process is designed to let you restart whenever +you wish and be incredibly quick to restart. Since we don't have the delay to start up shards anymore, your code becomes +reloaded instantly. + +## Creating Event Handlers + +Create a file path like `src/bot/mod.ts`. + +```ts +import { DISCORD_TOKEN } from '../../configs.ts' +import { Collection, createBot, Intents } from '../../deps.ts' +import { psql } from './cache/mod.ts' + +export const bot = createBot({ + token: DISCORD_TOKEN, + botId: 270010330782892032n, + intents: Intents.Guilds | Intents.GuildMessages, + events: { + messageCreate: function (bot, message) { + console.log('message arrived') + }, + }, +}) +``` + +Alright that was a lot of code. Now let's break it down little by little. + +### Understanding createBot() + +**Basic Keys** + +- `token` if you can't figure this out stop reading and find another guide please. Thanks. +- `botId` This is going to be your bot id. The reason we require this here is because we are going to set up a + standalone gateway process. With most other libs, they can fill this information using the READY event. However, since + our gateway is designed not to reboot, we are not going to get the READY event whenever we restart our bot. This means + we won't be able to fill this information later. Another method to get the id is to use the `token` but discord + developers have mentioned that this behavior is not documented and not supposed to be relied on to remain stable. Due + to these reasons, we chose to just require the bot id be passed here. +- `applicationId` is an optional choice if your bot is old and has a unique id different from it's bot id. +- `intents`: Provide the intents you like using a bitwise OR operation (eg. `Intents.Guilds | Intents.GuildsMessages`). + String form supports autocomplete and type safety. +- `events`: These are your event handler functions. When a MESSAGE_CREATE event arrives from Discord it will be + processed here. We will set up the routing to run these functions later in the guide but for now you can see how to + set it up. Note, you can create these functions in separate files and just import them here as you wish. + +## Using Your Cache + +Since we are using a standalone gateway, a custom cache is essentially required as explained in step 3 of this guide. +Here we'll have some basic functions to make use of the cache we created in step 3. + +```ts +const cache = { + /** Get a single item from the table */ + async get(key) { + return await psql`SELECT * FROM ${psql( + tables[table], + )} WHERE "id" = ${psql.types.bigint(key)}` + }, + /** Completely empty this table. */ + async clear() { + await psql`TRUNCATE TABLE ${psql(tables[table])}` + }, + /** Delete the data related to this key from table. */ + async delete(key) { + await psql`DELETE FROM ${psql( + tables[table], + )} WHERE "id" = ${psql.types.bigint(key)}` + return true + }, + /** Check if there is data assigned to this key. */ + async has(key) { + return Boolean( + await psql`SELECT 1 FROM ${psql( + tables[table], + )} WHERE "id" = ${psql.types.bigint(key)}`, + ) + }, + /** Check how many items are stored in this table. */ + async size() { + return (await psql`SELECT COUNT("id") FROM ${psql(tables[table])}`).count + }, + /** Store new data to this table. */ + async set(key, data) { + await psql`INSERT INTO ${psql(tables[table])} ${psql( + data, + ...Object.keys(data), + )}` + return true + }, + // THESE TWO ARE USELESS FOR CUSTOM CACHE BUT NEED TO SHUT UP TS ERRORS + async forEach(callback) {}, + async filter(callback) { + return new Collection() + }, +} +``` + +You can insert any code you desire for your cache system here. Since we were using PGSQL, we used sql queries to make +these requests. However, should you need to communicate to Redis or anything else of your choice, you can do so here. + +> Note: The .filter() and .forEach() methods are unnecessary and should not be used for your bot as they are not +> optimized for performance. These are made for smaller bot users who would not leave itoh alone and in order to please +> them itoh gave them their hearts desire! LMAO! + +## Customizing Internal Code + +One of the best parts about discordeno is the flexibility. In order to show this off, we will use the `user` example but +you can apply this to any part of the library. + +### Why Is Customizing Important? + +At large scale, every single property can become expensive to store in your cache. For example, if your bot does not +make use of a `channel.topic` why storing potentially millions of strings in your memory for something you never +need/user. This could save you potentially GBs of memory to just remove this one property. + +### Customizing Process + +First, let's create a file in some path like `src/bot/internals/mod.ts`. Note that we will create quite a few files +below simply to keep code cleaner and simpler, in expectation that it will grow more complex later. You can merge them +as you wish. + +```ts +import { Bot } from '../../../deps.ts' +import { customizeBotTransformers } from './transformers/mod.ts' + +export function customizeBotInternals(bot: Bot) { + bot = customizeBotTransformers(bot) + // ADD AS MANY MORE CUSTOMIZATIONS HERE AS YOU LIKE TO HANDLERS, HELPERS, UTILS ETC... + return bot +} +``` + +We also need to add another file now at `src/bot/internals/transformers/mod.ts` + +```ts +import { Bot } from '../../../../deps.ts' +import { customizeUserTransformer } from './user.ts' + +export function customizeBotTransformers(bot: Bot) { + bot = customizeUserTransformer(bot) + // ADD ANY MORE CUSTOM TRANSFORMERS HERE + return bot +} +``` + +One more file at `src/bot/internals/transformers/user.ts` + +```ts +import { Bot, DiscordenoUser, transformUser } from '../../../../deps.ts' + +export function customizeUserTransformer(bot: Bot) { + bot.transformers.user = function (bot, payload) { + // REMOVE USELESS PROPS OUR BOT DOESNT USE + const { + system, + locale, + verified, + email, + flags, + mfaEnabled, + premiumType, + publicFlags, + ...user + } = transformUser(bot, payload) + + // RETURN ONLY USEFUL PROPS WE NEED TO USE AND CACHE IF NECESSARY + return user as DiscordenoUser + } + + return bot +} +``` + +First we override the internal transformer for the `user` object. What's cool is the typings will be automatically +provided :) Next, we use the `transformUser` function from the lib itself to make it create the internal user version. +The reason I do this is so when I update the library and a new property is added or removed i can simply update and get +it. Should you desire maximum control you can remove this entirely and only have what you want no matter what discord +sends. Discordeno gives you the ability to stay in control. + +This method can be applied to any transformer, helper function, gateway event handler, util function or any part of the +library. Anything and everything is possible to override. You do NOT need to fork and modify the library ever and give +yourself a headache trying to maintain your fork with updates. + +## Handling Incoming Gateway Events + +Remember, this is a separate process we need to make sure we are listening to incoming events from our gateway +instances. Since we used http in our Gateway step, we can create an http listener here as well. + +Create a file in a path like `src/bot/gatewayEventsListener.ts` + +Now we should create a http listener, check for authorization in headers, run `bot.events.raw` and `bot.handlers[event]` + +```ts +import { DiscordGatewayPayload } from 'discordeno' +import { EVENT_HANDLER_PORT, REST_AUTHORIZATION } from '../../configs.ts' + +const server = Deno.listen({ port: EVENT_HANDLER_PORT }) + +// Connections to the server will be yielded up as an async iterable. +for await (const conn of server) { + // In order to not be blocking, we need to handle each connection individually + // in its own async function. + handleRequest(conn) +} + +async function handleRequest(conn: Deno.Conn) { + // This "upgrades" a network connection into an HTTP connection. + const httpConn = Deno.serveHttp(conn) + // Each request sent over the HTTP connection will be yielded as an async + // iterator from the HTTP connection. + for await (const requestEvent of httpConn) { + if ( + !REST_AUTHORIZATION || + REST_AUTHORIZATION !== requestEvent.request.headers.get('AUTHORIZATION') + ) { + return requestEvent.respondWith( + new Response(JSON.stringify({ error: 'Invalid authorization key.' }), { + status: 401, + }), + ) + } + + const json = (await requestEvent.request.json()) as { + message: DiscordGatewayPayload + shardId: number + } + + // Run raw event. + bot.events.raw(bot, json.message, json.shardId) + + if (json.message.t && json.message.t !== 'RESUMED') { + // When a guild or something isn't in cache this will fetch it before doing anything else. + if (!['READY', 'GUILD_LOADED_DD'].includes(json.message.t)) { + await bot.events.dispatchRequirements(bot, json.message, json.shardId) + } + + // Run event function provided in bot.events + bot.handlers[json.message.t]?.(bot, json.message, json.shardId) + } + + new Response(undefined, { status: 200 }) + } +} +``` diff --git a/website/old_docs/big-bot-guide/gateway.md b/website/old_docs/big-bot-guide/gateway.md new file mode 100644 index 000000000..1df34317d --- /dev/null +++ b/website/old_docs/big-bot-guide/gateway.md @@ -0,0 +1,1076 @@ +--- +sidebar_position: 3 +sidebar_label: Step 2 - Gateway +--- + +# Step 2: Creating A Standalone Gateway Process + +If you are reading this, you should have your REST process completed. We are going to need it here. This process will be +connecting to discord's websockets which will send you all the events. + +Before, we dive into how, here is a quick summary of why you will want a standalone gateway process. + +## Why Use Standalone REST Process? + +- **Zero Downtime Updates**: + + - Your bot can be updated in a matter of seconds. With normal sharding, you have to restart which also has to process + identifying all your shards with a 1/~5s rate limit. With WS handling moved to a proxy process, this allows you to + instantly get the bot code restarted without any concerns of delays. If you have a bot on 200,000 servers normally + this would mean a 20 minute delay to restart your bot if you made a small change and restarted. + +- **Zero Downtime Resharding**: + + - Discord stops letting your bot get added to new servers at certain points in time. For example, suppose you had + 150,000 servers running 150 shards. The maximum amount of servers your shards could hold is 150 \* 2500 = 375,000. + If your bot reaches this, it can no longer join new servers until it re-shards. + - DD proxy 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. + +- **Horizontal Scaling**: + + - The proxy system allows you to scale the bot horizontally. When you reach a huge size, you can either keep spending + more money to keep beefing up your server or you can buy several cheaper servers and scale horizontally. The proxy + means you can have WS handling on a completely separate system. + +- **No Loss Restarts**: + + - When you restart a bot without the proxy system, normally you would lose many events. Users may be using commands or + messages are sent that will not be filtered. As your bot's grow this number rises dramatically. Users may join who + wont get the auto-roles or any other actions your bot should take. With the proxy system, you can keep restarting + your bot and never lose any events. Events will be put into a queue while your bot is down(max size of queue is + customizable), once the bot is available the queue will begin processing all events. + +- **Controllers**: + + - The controller aspect gives you full control over everything inside the proxy. You can provide a function to simply + override the handler. For example, if you would like a certain function to do something different, instead of having + to fork and maintain your fork, you can just provide a function to override. + +- **Clustering With Workers**: + - Take full advantage of all your CPU cores by using workers to spread the load. Control how many shards per worker + and how many workers to maximize efficiency! + +## Creating Gateway Manager + +Create a file under some path like `src/gateway/mod.ts`. + +```ts +import { DISCORD_TOKEN, REST_AUTHORIZATION, REST_PORT } from '../../configs.ts' +import { BASE_URL, createRestManager } from '../../deps.ts' + +const rest = createRestManager({ + token: DISCORD_TOKEN, + secretKey: REST_AUTHORIZATION, + customUrl: `http://localhost:${REST_PORT}`, +}) +``` + +Throw another rest manager here which will be responsible for calling the main REST process we created in Step 1. This +will allow your gateway to communicate to the other process. Remember this is just to communicate outwards, this file +should not have the http listener. + +> Feel free to refactor and optimize this should you wish to move `const rest...` to a separate file and reuse in both +> steps. + +### Getting Gateway Bot Data + +Now we need to use this rest manager to call the api to get information about how to connect to discord's gateway for +your bot. + +```ts +import { routes } from '../../deps.ts' + +const rest = createRestManager({ + token: DISCORD_TOKEN, + secretKey: REST_AUTHORIZATION, + customUrl: `http://localhost:${REST_PORT}`, +}) + +// CALL THE REST PROCESS TO GET GATEWAY DATA +const gatewayBot = await rest + .runMethod(rest, 'GET', routes.GATEWAY_BOT()) + .then(res => ({ + url: res.url, + shards: res.shards, + sessionStartLimit: { + total: res.session_start_limit.total, + remaining: res.session_start_limit.remaining, + resetAfter: res.session_start_limit.reset_after, + maxConcurrency: res.session_start_limit.max_concurrency, + }, + })) +``` + +With this info, we can now create our gateway manager. + +### Understanding Gateway Manager + +```ts +import { INTENTS, SHARDS_PER_WORKER, TOTAL_WORKERS } from '../../configs.ts' + +const gateway = createGatewayManager({ + gatewayBot, + gatewayConfig: { + token: DISCORD_TOKEN, + intents: INTENTS, + }, + totalShards: gatewayBot.shards, + shardsPerWorker: SHARDS_PER_WORKER, + totalWorkers: TOTAL_WORKERS, + // debug: console.log, + // THIS WILL BE USED LATER IN WORKER SO LEAVE IT HERE + handleDiscordPayload: () => {}, +}) +``` + +#### Basic Keys + +- `EVENT_HANDLER_SECRET_KEY` is from your configs that will be used to make sure requests sent to your event handler + process are indeed from you. +- `DISCORD_TOKEN` if you can't figure this out, this guide isn't for you. Please find another. +- `INTENTS` pass in a number or a string of intents. Autocomplete/type-safety is provided for strings :) + +#### Powerful Keys + +If your bot is going to be run on one process, you can re-use the data that discord gave you to connect. + +- `totalShards`: is the maximum number of shards you want to use for connecting your bot. Should you think Discord is + not smart enough to recommend a good amount, use this to override their choice. Highly recommend just using theirs. +- `lastShardId`: is the last shard you want to connect in this process. + - Using a combination of `lastShardId` & `firstShardId`, you can create several processes or even several servers to + handle different amounts of shards should your bot get that big to require horizontal scaling. You can control how + many shards each gateway manager will be responsible for. +- `spawnShardDelay`: The delay in milliseconds to wait before spawning next shard. +- `shardsPerWorker`: The amount of shards to load per worker. Discussed in detail below. +- `totalWorkers`: The maximum amount of workers to use for your bot. + +#### Gateway Cache + +There is a few things that we cache in the gateway process directly, because sending them across the network is not +ideal. This is done to support custom cache functionality. + +- `guildIds`: Used for determining what type of GUILD_CREATE event is received. +- `loadingGuildIds`: Used for determining if all guilds have arrived when initially connecting. +- `editedMessages`: Used to prevent spam of events across the network. MESSAGE_UPDATE are an extremely heavy event. Any + embed or link that is in a message will unfurl triggerring a message update event. This is undesired behavior for 99% + of bots out there. If someone sends a message with 5 urls, in there you will get a MESSAGE_CREATE and 5 MESSAGE_UPDATE + events. If that user edits a single letter on it you now get 6 MESSAGE_UPDATE events, 1 for the content change and 5 + more for each url being unfurled. The editedMessages cache checks if the content of the message changed or not before + sending the event downstream. Override this behavior if you need different behavior. + +#### Gateway Method Overriding + +One of the benefits of Discordeno is that you can override/customize anything from the library. Should you desire to +change the logic in any method it is as simple as: + +```ts +// TYPINGS WILL BE AUTOMATICALLY PROVIDED +gateway.heartbeat = function (gateway, shardId, interval) { + // YOUR CUSTOM HANDLING CODE HERE +} +``` + +## Workers + +Now, we should take a minute here to talk about workers. Workers are just Clusters in Node.js + +When you have a big bot and you are processing millions of events, you need to speed up that processing. Keeping it in 1 +thread is not very nice since JavaScript is single threaded. This means it can only process 1 event at a time. With +workers, you can make it process several events at the same time. We mentioned the `shardsPerWorker` earlier. This +option was added to allow you to choose how many shards should be managed by each worker. + +When shards are spawned, they are triggered by a method on gateway: `tellWorkerToIdentify`, so we'll have to modify it +to create workers and send message: + +```ts +gateway.tellWorkerToIdentify = async ( + _gateway, + workerId, + shardId, + _bucketId, +) => { + let worker = workers.get(workerId) + if (!worker) { + worker = createWorker(workerId) + workers.set(workerId, worker) + } + + // TYPE TYPE WorkerMessage IS FROM WORKER FILE, DISCUSSED IN DETAIL BELOW + const identify: WorkerMessage = { + type: 'IDENTIFY_SHARD', + shardId, + } + + worker.postMessage(identify) +} +``` + +You can choose to replace the handler with any desired functionality you like. For example, should should you want to +create a new worker for each new workerId that appears and have that worker trigger the identify functionaly. How you +choose to handler workers is left in your care. + +Now that we've setup our initial gateway manager and added `tellWorkerToIdentify` to `gateway`, we need to do the rest +of the work: creating workers, spawning shards etc. + +```ts +import { EVENT_HANDLER_SECRET_KEY, EVENT_HANDLER_URL } from '../../configs.ts' +import { Worker } from 'worker_threads' +import { + WorkerCreateData, + WorkerGetShardInfo, + WorkerMessage, + WorkerShardInfo, + WorkerShardPayload, +} from './worker.js' + +// A COLLECTION OF WORKERS +const workers = new Collection() +const nonces = new Collection void>() + +function createWorker(workerId: number) { + const workerData: WorkerCreateData = { + intents: gateway.manager.gatewayConfig.intents ?? 0, + token: DISCORD_TOKEN, + // TODO: PUT THIS SEPARATELY. CAN USE MULTIPLE URLS IF YOU HAVE MULTIPLE BOT PROCESSES HANDLING DIFFERENT SHARDS' EVENTS + handlerUrls: [EVENT_HANDLER_URL], + handlerAuthorization: EVENT_HANDLER_SECRET_KEY, + path: './worker.ts', + totalShards: gateway.manager.totalShards, + workerId, + } + + const worker = new Worker('./worker.js', { + workerData, + }) + + worker.on('message', async (data: ManagerMessage) => { + switch (data.type) { + case 'REQUEST_IDENTIFY': { + await gateway.manager.requestIdentify(data.shardId) + + const allowIdentify: WorkerMessage = { + type: 'ALLOW_IDENTIFY', + shardId: data.shardId, + } + + worker.postMessage(allowIdentify) + + break + } + case 'NONCE_REPLY': { + nonces.get(data.nonce)?.(data.data) + } + } + }) + + return worker +} + +// TYPES WE USE +export type ManagerMessage = + | ManagerRequestIdentify + | ManagerNonceReply + +export type ManagerRequestIdentify = { + type: 'REQUEST_IDENTIFY' + shardId: number +} + +export type ManagerNonceReply = { + type: 'NONCE_REPLY' + nonce: string + data: T +} +``` + +## Spawning Shards + +Once you are ready and the gateway has been created as you desired, we can begin spawning the shards. + +```ts +gateway.spawnShards(gateway) +``` + +This code now handles creating gateway manager, creating workers, spawning shards and sending the info to each workers. +Next is creating a worker file to receive these info, connecting to gateway and sending the events to bot process. + +Here's the full code of `src/gateway/mod.ts`: + +```ts +import { + DISCORD_TOKEN, + EVENT_HANDLER_SECRET_KEY, + EVENT_HANDLER_URL, + INTENTS, + REST_AUTHORIZATION, + REST_PORT, + SHARDS_PER_WORKER, + TOTAL_WORKERS, +} from '../../configs.ts' +import { BASE_URL, createRestManager, routes } from '../../deps.ts' +import { Worker } from 'worker_threads' +import { + WorkerCreateData, + WorkerGetShardInfo, + WorkerMessage, + WorkerShardInfo, + WorkerShardPayload, +} from './worker.ts' + +const rest = createRestManager({ + token: DISCORD_TOKEN, + secretKey: REST_AUTHORIZATION, + customUrl: `http://localhost:${REST_PORT}`, +}) + +const rest = createRestManager({ + token: DISCORD_TOKEN, + secretKey: REST_AUTHORIZATION, + customUrl: `http://localhost:${REST_PORT}`, +}) + +// CALL THE REST PROCESS TO GET GATEWAY DATA +const gatewayBot = await rest + .runMethod(rest, 'GET', routes.GATEWAY_BOT()) + .then(res => ({ + url: res.url, + shards: res.shards, + sessionStartLimit: { + total: res.session_start_limit.total, + remaining: res.session_start_limit.remaining, + resetAfter: res.session_start_limit.reset_after, + maxConcurrency: res.session_start_limit.max_concurrency, + }, + })) + +const gateway = createGatewayManager({ + gatewayBot, + gatewayConfig: { + token: DISCORD_TOKEN, + intents: INTENTS, + }, + totalShards: gatewayBot.shards, + shardsPerWorker: SHARDS_PER_WORKER, + totalWorkers: TOTAL_WORKERS, + // debug: console.log, + handleDiscordPayload: () => {}, + tellWorkerToIdentify: async (_gateway, workerId, shardId, _bucketId) => { + let worker = workers.get(workerId) + if (!worker) { + worker = createWorker(workerId) + workers.set(workerId, worker) + } + + // TYPE TYPE WorkerMessage IS FROM WORKER FILE, DISCUSSED IN DETAIL BELOW + const identify: WorkerMessage = { + type: 'IDENTIFY_SHARD', + shardId, + } + + worker.postMessage(identify) + }, +}) + +// A COLLECTION OF WORKERS +const workers = new Collection() +const nonces = new Collection void>() + +function createWorker(workerId: number) { + const workerData: WorkerCreateData = { + intents: gateway.manager.gatewayConfig.intents ?? 0, + token: DISCORD_TOKEN, + handlerUrls: [EVENT_HANDLER_URL], + handlerAuthorization: EVENT_HANDLER_SECRET_KEY, + path: './worker.ts', + totalShards: gateway.manager.totalShards, + workerId, + } + + const worker = new Worker('./worker.ts', { + workerData, + }) + + worker.on('message', async (data: ManagerMessage) => { + switch (data.type) { + case 'REQUEST_IDENTIFY': { + await gateway.manager.requestIdentify(data.shardId) + + const allowIdentify: WorkerMessage = { + type: 'ALLOW_IDENTIFY', + shardId: data.shardId, + } + + worker.postMessage(allowIdentify) + + break + } + case 'NONCE_REPLY': { + nonces.get(data.nonce)?.(data.data) + } + } + }) + + return worker +} + +// TYPES WE USE +export type ManagerMessage = + | ManagerRequestIdentify + | ManagerNonceReply + +export type ManagerRequestIdentify = { + type: 'REQUEST_IDENTIFY' + shardId: number +} + +export type ManagerNonceReply = { + type: 'NONCE_REPLY' + nonce: string + data: T +} + +// SPAWN SHARDS INTO WORKERS +gateway.spawnShards() +``` + +## Worker File + +Now that we've handled creating gateway, workers, we need to create a worker file to identify, receive gateway events +and send them to bot process. + +Create a file in a path like `src/gateway/worker.ts`. + +Now we'll have to create a Shard Manager, this is what will handle identifying, receiving events. + +```ts +import { createShardManager } from 'discordeno' +import { parentPort, workerData } from 'worker_threads' + +if (!parentPort) { + throw new Error('Parent port is null') +} + +// THE DATA WE GET FROM GATEWAY FILE +const script: WorkerCreateData = workerData + +const identifyPromises = new Map void>() + +const manager = createShardManager({ + gatewayConfig: { + intents: script.intents, + token: script.token, + }, + shardIds: [], + totalShards: script.totalShards, + // WE WILL COVER THESE TWO FUNCTIONS IN LATER PART OF THE GUIDE, FOR NOW, LEAVE IT THIS WAY + handleMessage: () => {}, + requestIdentify: async () => {}, +}) +``` + +The above code only creates a shard manager, we now have 3 more things to do: + +- Listening to gateway process, sending events received to respective shards in the manager. +- Handling Discord Payloads. +- Requesting Identify. + +## Sending events from Gateway to Shards in Manager + +In order for the shards to receive events and send to bot process, we need to receive the event payloads from gateway +first, we can do this by using `message` event in `parentPort` like shown below: + +```ts +import { Shard } from 'discordeno' + +function buildShardInfo(shard: Shard): WorkerShardInfo { + return { + workerId: script.workerId, + shardId: shard.id, + rtt: shard.heart.rtt || -1, + state: shard.state, + } +} + +parentPort.on('message', async (data: WorkerMessage) => { + switch (data.type) { + // Gateway sends IDENTIFY_SHARD in gateway.tellWorkerToIdentify + case 'IDENTIFY_SHARD': { + await manager.identify(data.shardId) + + break + } + // Gateway sends ALLOW_IDENTIFY when worker requests to identify + case 'ALLOW_IDENTIFY': { + identifyPromises.get(data.shardId)?.() + identifyPromises.delete(data.shardId) + + break + } + // Gateway sends SHARD_PAYLOAD for every events it receives from Discord + case 'SHARD_PAYLOAD': { + manager.shards.get(data.shardId)?.send(data.data) + + break + } + // Send shard info if gateway sends GET_SHARD_INFO + case 'GET_SHARD_INFO': { + const infos = manager.shards.map(buildShardInfo) + + parentPort?.postMessage({ + type: 'NONCE_REPLY', + nonce: data.nonce, + data: infos, + }) + } + } +}) +``` + +Now TypeScript will error because of missing types, add these to your code: + +```ts +import { ShardSocketRequest, ShardState } from 'discordeno' + +export type WorkerMessage = + | WorkerIdentifyShard + | WorkerAllowIdentify + | WorkerShardPayload + | WorkerGetShardInfo + +export type WorkerIdentifyShard = { + type: 'IDENTIFY_SHARD' + shardId: number +} + +export type WorkerAllowIdentify = { + type: 'ALLOW_IDENTIFY' + shardId: number +} + +export type WorkerShardPayload = { + type: 'SHARD_PAYLOAD' + shardId: number + data: ShardSocketRequest +} + +export type WorkerGetShardInfo = { + type: 'GET_SHARD_INFO' + nonce: string +} + +export type WorkerCreateData = { + intents: number + token: string + handlerUrls: string[] + handlerAuthorization: string + path: string + totalShards: number + workerId: number +} + +export type WorkerShardInfo = { + workerId: number + shardId: number + rtt: number + state: ShardState +} +``` + +## Handling Discord Payloads + +One of the big things we didn't cover yet is the handler for discord payloads. This is the main sauce of your worker +process here. This is going to take the events that the gateway manager sent and send it to your event handler. How you +wish to communicate with your event handler is up to you. For this guide, we will use http, but you can replace that +with anything you like. + +```ts +manager.createShardOptions.handleMessage = async (shard, message) => { + const url = script.handlerUrls[shard.id % script.handlerUrls.length] + if (!url) return console.error('ERROR: NO URL FOUND TO SEND MESSAGE') + + await fetch(url, { + method: 'POST', + body: JSON.stringify({ message, shardId: shard.id }), + headers: { + 'Content-Type': 'application/json', + Authorization: script.handlerAuthorization, + }, + }).catch(error => console.error(error)) +} +``` + +You can change this function to use a WS or any form of communication you prefer to use to send this to your event +handler. + +This is also the place where you make use of the [Gateway Cache](#gateway-cache) we mentioned earlier (`guildIds`, +`loadingGuildIds`, `editedMessages`). + +## Gateway Queue + +One thing we can add on here, which you will find already done in the template if you are using it. However, it is still +good to read this to learn and understand the logic behind it. When you need a downtime for whatever reason, you can +create a queue like system to avoid any missed events. Let's create a simple queue. If it errors, assuming something +like the bot event listener process is down for whatever reason, the `.catch` in `fetch` will run adding this event to +the queue to try again in one second by calling the `handleQueue` function. + +```ts +.catch(() => { + // IF FAILED TRY TO QUEUE MAYBE LISTENER IS DOWN + if (message.t === "INTERACTION_CREATE") handleInteractionQueueing(message, shard.id); + else queue.events.push({ shardId: shard.id, message }); + + setTimeout(handleQueue, 1000); +}); +``` + +Now TypeScript will probably throw some errors at your face, so let's fix those real quick. Create an object that will +hold the queue of events for our gateway. + +```ts +import { DiscordGatewayPayload } from 'discordeno' + +const queue: GatewayQueue = { + processing: false, + events: [], +} + +export interface QueuedEvent { + message: DiscordGatewayPayload + shardId: number +} + +export interface GatewayQueue { + processing: boolean + events: QueuedEvent[] +} + +async function handleQueue() { + // PLACEHOLDER FUNCTION THAT WILL HANDLE PROCESSING THE QUEUE +} + +async function handleInteractionQueueing( + message: DiscordGatewayPayload, + shardId: number, +) { + // PLACEHOLDER FUNCTION +} +``` + +Alrighty, since TypeScript stopped being annoying, let's continue. Next, we should make sure to avoid fetching when the +queue is already processing or has events queued up. This will help us preserve the order of events in the queue. + +```ts +handleMessage: async function (shard, message) { +// IF QUEUE IS RUNNING JUST ADD TO QUEUE +if (queue.processing) { + if (message.t === "INTERACTION_CREATE") return handleInteractionQueueing(message, shard.id); + + return queue.events.push({ shardId: shard.id, message }); +} + +await fetch(EVENT_HANDLER_URL, { +``` + +Typescript must be at it again so let's shut it up again. Keep in mind that we are handling interaction events +separately because they require a response within 3 seconds or they will become invalid. In this function first we +automatically respond to the ones that can not be deferred. For the interactions that can be deferred, we will simply +defer them and add this event to the queue. + +```ts +import { + DiscordInteraction, + InteractionResponseTypes, + InteractionTypes, + routes, +} from 'discordeno' +import { BOT_SERVER_INVITE_CODE } from '../../configs.ts' + +async function handleInteractionQueueing( + message: DiscordGatewayPayload, + shardId: number, +) { + if (message.t !== 'INTERACTION_CREATE') return + + const interaction = message.d as DiscordInteraction + // IF THIS INTERACTION IS NOT DEFERABLE + if ( + [ + InteractionTypes.ModalSubmit, + InteractionTypes.ApplicationCommandAutocomplete, + ].includes(interaction.type) + ) { + return await rest.runMethod( + rest, + 'POST', + routes.INTERACTION_ID_TOKEN(BigInt(interaction.id), interaction.token), + { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: { + content: `The bot is having a temporary issue, please try again or contact us at https://discord.gg/${BOT_SERVER_INVITE_CODE}`, + }, + }, + ) + } + + await rest.runMethod( + rest, + 'POST', + endpoints.INTERACTION_ID_TOKEN(BigInt(interaction.id), interaction.token), + { + // MESSAGE COMPONENTS NEED SPECIAL DEFER + type: + InteractionTypes.MessageComponent === interaction.type + ? InteractionResponseTypes.DeferredUpdateMessage + : InteractionResponseTypes.DeferredChannelMessageWithSource, + }, + ) + + // ADD EVENT TO QUEUE + queue.events.push({ shardId, message }) +} +``` + +Oh no, TypeScript is at it again. We need to make a REST manager so that our gateway proxy can communicate with our REST +proxy. We then can make use of it to send a POST request. + +```ts +const rest = createRestManager({ + token: DISCORD_TOKEN, + secretKey: REST_AUTHORIZATION, + customUrl: REST_URL, +}) +``` + +So now there is only one thing left the `handleQueue` function. First we get the first item from the queue using +`.shift()`. Then we check to see if that item exists. If it does not exist, we mark the queue as no longer processing +and cancel out. However, if it does exist, we send a fetch request to the bot event handler process. In the `.catch()` +we will add this event back in to the start of the queue in case the bot is still down. Finally we call this function +again to run the next item in the queue. + +```ts +async function handleQueue() { + const event = queue.events.shift() + // QUEUE IS EMPTY + if (!event) { + console.log('GATEWAY QUEUE ENDING') + queue.processing = false + return + } + + await fetch(EVENT_HANDLER_URL, { + headers: { + Authorization: EVENT_HANDLER_SECRET_KEY, + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify({ + shardId: event.shardId, + message: event.message, + }), + }) + .then(res => { + res.text() + handleQueue() + }) + .catch(() => { + // EVENT HANDLER STILL NOT ACCEPTING REQUEST. SO ADD BACK TO QUEUE + queue.events.unshift(event) + setTimeout(handleQueue, 1000) + }) +} +``` + +## Requesting Identify + +We need to request identify in order to trigger initial handshake with the gateway, we'll use `manager.requestIdentify` +to do this. + +```ts +import { ManagerMessage } from './mod.ts' + +manager.requestIdentify = async function (shardId: number): Promise { + return await new Promise(resolve => { + identifyPromises.set(shardId, resolve) + + const identifyRequest: ManagerMessage = { + type: 'REQUEST_IDENTIFY', + shardId, + } + + parentPort?.postMessage(identifyRequest) + }) +} +``` + +That's all, you've now setup your gateway and worker. Here's the full code of `src/gateway/worker.ts`: + +```ts +import { + createRestManager, + createShardManager, + DiscordGatewayPayload, + DiscordInteraction, + InteractionResponseTypes, + InteractionTypes, + routes, + Shard, + ShardSocketRequest, + ShardState, +} from 'discordeno' +import { parentPort, workerData } from 'worker_threads' +import { ManagerMessage } from './mod' +import { + BOT_SERVER_INVITE_CODE, + DISCORD_TOKEN, + EVENT_HANDLER_SECRET_KEY, + EVENT_HANDLER_URL, + REST_AUTHORIZATION, + REST_URL, +} from '../../configs.ts' + +if (!parentPort) { + throw new Error('Parent port is null') +} + +// THE DATA WE GET FROM GATEWAY FILE +const script: WorkerCreateData = workerData + +const identifyPromises = new Map void>() + +const rest = createRestManager({ + token: DISCORD_TOKEN, + secretKey: REST_AUTHORIZATION, + customUrl: REST_URL, +}) + +const manager = createShardManager({ + gatewayConfig: { + intents: script.intents, + token: script.token, + }, + shardIds: [], + totalShards: script.totalShards, + handleMessage: async (shard, message) => { + const url = script.handlerUrls[shard.id % script.handlerUrls.length] + if (!url) return console.error('ERROR: NO URL FOUND TO SEND MESSAGE') + + await fetch(url, { + method: 'POST', + body: JSON.stringify({ message, shardId: shard.id }), + headers: { + 'Content-Type': 'application/json', + Authorization: script.handlerAuthorization, + }, + }).catch(() => { + // IF FAILED TRY TO QUEUE MAYBE LISTENER IS DOWN + if (message.t === 'INTERACTION_CREATE') + handleInteractionQueueing(message, shard.id) + else queue.events.push({ shardId: shard.id, message }) + + setTimeout(handleQueue, 1000) + }) + }, + requestIdentify: async function (shardId: number): Promise { + return await new Promise(resolve => { + identifyPromises.set(shardId, resolve) + + const identifyRequest: ManagerMessage = { + type: 'REQUEST_IDENTIFY', + shardId, + } + + parentPort?.postMessage(identifyRequest) + }) + }, +}) + +function buildShardInfo(shard: Shard): WorkerShardInfo { + return { + workerId: script.workerId, + shardId: shard.id, + rtt: shard.heart.rtt || -1, + state: shard.state, + } +} + +parentPort.on('message', async (data: WorkerMessage) => { + switch (data.type) { + // Gateway sends IDENTIFY_SHARD in gateway.tellWorkerToIdentify + case 'IDENTIFY_SHARD': { + await manager.identify(data.shardId) + + break + } + // Gateway sends ALLOW_IDENTIFY when worker requests to identify + case 'ALLOW_IDENTIFY': { + identifyPromises.get(data.shardId)?.() + identifyPromises.delete(data.shardId) + + break + } + // Gateway sends SHARD_PAYLOAD for every events it receives from Discord + case 'SHARD_PAYLOAD': { + manager.shards.get(data.shardId)?.send(data.data) + + break + } + // Send shard info if gateway sends GET_SHARD_INFO + case 'GET_SHARD_INFO': { + const infos = manager.shards.map(buildShardInfo) + + parentPort?.postMessage({ + type: 'NONCE_REPLY', + nonce: data.nonce, + data: infos, + }) + } + } +}) + +const queue: GatewayQueue = { + processing: false, + events: [], +} + +async function handleQueue() { + const event = queue.events.shift() + // QUEUE IS EMPTY + if (!event) { + console.log('GATEWAY QUEUE ENDING') + queue.processing = false + return + } + + await fetch(EVENT_HANDLER_URL, { + headers: { + Authorization: EVENT_HANDLER_SECRET_KEY, + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify({ + shardId: event.shardId, + message: event.message, + }), + }) + .then(res => { + res.text() + handleQueue() + }) + .catch(() => { + // EVENT HANDLER STILL NOT ACCEPTING REQUEST. SO ADD BACK TO QUEUE + queue.events.unshift(event) + setTimeout(handleQueue, 1000) + }) +} + +async function handleInteractionQueueing( + message: DiscordGatewayPayload, + shardId: number, +) { + if (message.t !== 'INTERACTION_CREATE') return + + const interaction = message.d as DiscordInteraction + // IF THIS INTERACTION IS NOT DEFERABLE + if ( + [ + InteractionTypes.ModalSubmit, + InteractionTypes.ApplicationCommandAutocomplete, + ].includes(interaction.type) + ) { + return await rest.runMethod( + rest, + 'POST', + routes.INTERACTION_ID_TOKEN(BigInt(interaction.id), interaction.token), + { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: { + content: `The bot is having a temporary issue, please try again or contact us at https://discord.gg/${BOT_SERVER_INVITE_CODE}`, + }, + }, + ) + } + + await rest.runMethod( + rest, + 'POST', + routes.INTERACTION_ID_TOKEN(BigInt(interaction.id), interaction.token), + { + // MESSAGE COMPONENTS NEED SPECIAL DEFER + type: + InteractionTypes.MessageComponent === interaction.type + ? InteractionResponseTypes.DeferredUpdateMessage + : InteractionResponseTypes.DeferredChannelMessageWithSource, + }, + ) + + // ADD EVENT TO QUEUE + queue.events.push({ shardId, message }) +} + +export type WorkerMessage = + | WorkerIdentifyShard + | WorkerAllowIdentify + | WorkerShardPayload + | WorkerGetShardInfo + +export type WorkerIdentifyShard = { + type: 'IDENTIFY_SHARD' + shardId: number +} + +export type WorkerAllowIdentify = { + type: 'ALLOW_IDENTIFY' + shardId: number +} + +export type WorkerShardPayload = { + type: 'SHARD_PAYLOAD' + shardId: number + data: ShardSocketRequest +} + +export type WorkerGetShardInfo = { + type: 'GET_SHARD_INFO' + nonce: string +} + +export type WorkerCreateData = { + intents: number + token: string + handlerUrls: string[] + handlerAuthorization: string + path: string + totalShards: number + workerId: number +} + +export type WorkerShardInfo = { + workerId: number + shardId: number + rtt: number + state: ShardState +} + +export interface QueuedEvent { + message: DiscordGatewayPayload + shardId: number +} + +export interface GatewayQueue { + processing: boolean + events: QueuedEvent[] +} +``` + +Note, you can take this concept and expand on it as much as you like. You can swap out the fetch() with websockets or +any other system you like to communicate between your processes. I highly recommend you take some time to add checks in +place to prevent adding to queue when the queue reaches a certain size. You don't want this to become a memory leak of +infinite size and crash your gateway. So take the time to do this right in your setup. + +If you have any questions, please contact us on our [discord server](https://discord.gg/ddeno). diff --git a/website/old_docs/big-bot-guide/rest.md b/website/old_docs/big-bot-guide/rest.md new file mode 100644 index 000000000..ce419d2e9 --- /dev/null +++ b/website/old_docs/big-bot-guide/rest.md @@ -0,0 +1,134 @@ +--- +sidebar_position: 2 +sidebar_label: Step 1 - REST +--- + +# Creating A Standalone REST Process + +The first thing we want to make is our standalone REST process. This process will be used by almost every other process, +so it is going to be the foundation of the bot. + +Before, we dive into how, here is a quick summary of why you will want a standalone REST process. + +## Why Use Standalone REST Process? + +- Easily host on any serverless infrastructure. +- Freedom from global rate limit errors + - As your bot grows, you want to handle global rate limits better. Shards don't communicate fast enough to truly + handle it properly so this allows 1 rest handler across the entire bot. + - In fact, you can host multiple instances of your bot and all connect to the same rest server. +- REST does not rest! + - Separate rest means if your bot for whatever reason crashes, your requests that are queued will still keep going and + will not be lost. + - Seamless updates! When you want to update and reboot the bot, you could potentially lose tons of messages or + responses that are in queue. Using this you could restart your bot without ever worrying about losing any responses. +- Single source of contact to Discord API + - This will allow you to make requests to discord from anywhere including a bot dashboard. You no longer need to have + to communicate to your bot processes just to make a request or anything. Free up your bot process for processing bot + events. +- Scalability! Scalability! Scalability! + +## Preparations + +Before going further, you should have already made the following pieces: + +- rest/mod.ts +- deps.ts (Make sure to import discordeno) +- configs.ts +- Deno extension(if you are using deno, this is required) +- TabNine extension to make your life so much better. (Optional) + +## Creating Rest Manager + +Now let's open up that rest file and start coding. + +```ts +import { DISCORD_TOKEN, REST_AUTHORIZATION, REST_PORT } from '../../configs.ts' +import { BASE_URL, createRestManager } from '../../deps.ts' + +const rest = createRestManager({ + token: DISCORD_TOKEN, + secretKey: REST_AUTHORIZATION, + customUrl: `http://localhost:${REST_PORT}`, +}) +``` + +- `createRestManager` is imported from your deps file which should have exported everything from discordeno. +- `DISCORD_TOKEN` is the bot's token itself. +- `REST_AUTHORIZATION` is a special password you want to use to authenticate that requests being sent to your port are + indeed from you. +- `customUrl` the url where this rest process will be running. This can be localhost which we are using in this guide if + you want all processes on same VPS or separate them to different servers for horizontal scaling. `REST_PORT` is just + the port where you want the process hosted. + +Now you have an entire Rest manager ready and waiting. Only thing you need now, is to listen for requests. + +## Creating HTTP Listener + +Since this is not a beginner guide, I am assuming you know already how to create a HTTP listener. There are enough +guides on this out there. I will only cover the rough functionality. + +```ts +// START LISTENING TO THE URL(localhost) +const server = Deno.listen({ port: REST_PORT }) +console.info( + `HTTP webserver running. Access it at: http://localhost:${REST_PORT}/`, +) + +// Connections to the server will be yielded up as an async iterable. +for await (const conn of server) { + // In order to not be blocking, we need to handle each connection individually + // in its own async function. + handleRequest(conn) +} + +async function handleRequest(conn: Deno.Conn) { + // This "upgrades" a network connection into an HTTP connection. + const httpConn = Deno.serveHttp(conn) + // Each request sent over the HTTP connection will be yielded as an async + // iterator from the HTTP connection. + for await (const requestEvent of httpConn) { + if ( + !REST_AUTHORIZATION || + REST_AUTHORIZATION !== requestEvent.request.headers.get('AUTHORIZATION') + ) { + return requestEvent.respondWith( + new Response(JSON.stringify({ error: 'Invalid authorization key.' }), { + status: 401, + }), + ) + } + + const json = (await requestEvent.request.json()) as any + + // IMPLEMENT ANY ERROR HANDLING HERE IF YOU WOULD LIKE BY WRAPPING THIS IN A CATCH + + // MAKE THE REQUEST TO DISCORD + const result = await rest.runMethod( + rest, + // USE THE SAME METHOD THAT CAME IN. IF DELETE CAME IN WE SEND DELETE OUT + requestEvent.request.method as any, + // OVERWRITE THE CUSTOM URL WITH DISCORDS BASE URL + `${BASE_URL}/v${rest.version}${requestEvent.request.url.substring( + rest.customUrl.length, + )}`, + json, + ) + + // RETURN DISCORDS RESPONSE BACK TO THE PROCESS MAKING THE REQUEST + if (result) { + requestEvent.respondWith( + new Response(JSON.stringify(result), { + status: 200, + }), + ) + } else { + requestEvent.respondWith( + new Response(undefined, { + status: 204, + }), + ) + } + } +} +``` diff --git a/website/old_docs/big-bot-guide/step-by-step.md b/website/old_docs/big-bot-guide/step-by-step.md new file mode 100644 index 000000000..4ec7f3ae3 --- /dev/null +++ b/website/old_docs/big-bot-guide/step-by-step.md @@ -0,0 +1,40 @@ +--- +sidebar_position: 1 +--- + +# Step By Step Guide + +THIS IS A WORK IN PROGRESS GUIDE USING THE NEW v16 OF DISCORDENO. + +## Understanding The Goals of This Guide + +This guide is a quick-paced walkthrough meant for big bot developers. It is expected that you have a decent amount of +understanding of how to code your bots. + +## Is This Guide Meant For You? + +If your goal is not to have a bot in millions of discord servers, please find another guide/library. Discordeno is +heavily opinionated towards optimizing for bots at scale. If you do not know what a Map or a Set is without having to +google it, you are at the wrong place. + +## Why You Should Use Discordeno? + +The best way I can describe why you should use Discordeno, is from the words of the biggest bot developers themselves. +After speaking to some of the developers of the biggest JS/TS bots, you begin to see a pattern of users unhappy with the +current state of JS/TS libraries. They are no longer able to help them scale easily and are starting to move away to +other libraries or having to make their own libraries because they need to be able to make their bot distributed. + +The following quotes are from developers who have bot's in atleast 1 million+ discord servers. + +- Flexibility like no other library. + - One of the big bot developers found that when their bot got too big, Eris was just very painful to optimize. + - "A pretty large hassle, I had to fork eris and modify it. There was a lot of interdependency on the values from + caches that made it difficult to remove properties "safely" without searching the entire codebase" + - When discovering how easy it was to do the same thing in Discordeno: + - "the convenience of being able to do so puts confidence in me that the lib is versatile so it'd certainly draw me + towards it" +- Scalability: Standalone Gateway, Rest, Event Handler, Commands, Cache and much more. + - "All this sound like a dream (especially when you currently use eris)" + +Discordeno provides you all the tools that you need to make bot development really easy. As the old saying goes, the +best way to learn to ride a bicycle is to actually try riding a bicycle. So let's try out Discordeno. diff --git a/website/old_docs/frequently-asked-questions.md b/website/old_docs/frequently-asked-questions.md new file mode 100644 index 000000000..791e6a479 --- /dev/null +++ b/website/old_docs/frequently-asked-questions.md @@ -0,0 +1,107 @@ +--- +sidebar_position: 5 +--- + +# Frequently Asked Questions + +## Does Discordeno Support TypeScript? + +TypeScript is supported to the highest standard by Discordeno. TypeScript is included in Discordeno since Deno supports it. This implies that before using TypeScript, you do not need to compile it. But this isn't the main reason Discordeno is the ideal library for TypeScript programmers. I was experimenting with a lot of various things when I created this library, and automatic typings was one of them. + +When I utilised other libraries, I frequently observed incorrect or troublesome typings. This is so that TypeScript won't alert the library developers because most of the Discord API typings aren't utilised by the libraries themselves. + +It is quite unlikely that these typings would become wrong or outdated as a result of minor errors like forgetting to update typings because Discordeno utilises them as part of the rest process. Libraries occasionally add a property without also adding it to their typings. Because of this, TypeScript developers cannot use it, only JavaScript developers can. Typings are crucial for TypeScript developers. Typings are treated as a component of the code by Discordeno! A breaking change in typings is a breaking change for the library! + +## How Stable Is Discordeno? + +Stability is one of the main problems with practically every library (I have used). None of the libraries showed TypeScript developers the love and care that they deserve. Because breaking changes to typings occasionally occurred without producing a MAJOR bump, TypeScript projects would occasionally fail. As a result, production TypeScript bots would fail. At times, I was the only one keeping the typings up to date for that library. Some libraries that were older than 1.0 didn't even have a stable branch or version, so I didn't have to worry about them undergoing breaking changes. + +The finest stability for TypeScript developers is one of my basic goals for this library. +No matter how little, a change that impacts the public API qualifies as a breaking change. I don't care whether we reach version 500. As a library maintainer, you should never be scared to bump a MAJOR because it just involves a tiny modification or a type change because doing so will ruin the end user's experience. + +## Why Doesn't Discordeno Use Classes or EventEmitter? + +This is a design decision for the library itself. You can still use class on your bot if you want. In fact, I hope someone +makes a framework/templates for this lib one day using classes so that devs have a choice on which style they prefer. +Without trying to write an entire thesis statement on the reasons why I avoided Classes in this library, I will just link to +the best resources that I believe help explain it. + +- [Really good article](https://dannyfritz.wordpress.com/2014/10/11/class-free-object-oriented-programming/) +- [Lecture by one of the developers who makes JavaScript](https://www.youtube.com/watch?v=PSGEjv3Tqo0) + +In regards to EventEmitter, I believe a functional event API was a much better choice. EventEmitter at its core, is simply a set of functions that run when a certain event is emitted. In Discordeno, that function is executed instead of emitting some event to trigger it. + +```typescript +// EventEmitter Example +EventEmitter.emit('guildCreate', guild) +// Discordeno Example +bot.events.guildCreate?.(bot, guild) +``` + +There isn't really any difference especially for users when they use it. One bad thing about EventEmitter is that if +misused it can easily cause memory leaks. It is very easy to open yourself up to these memory leak issues. It has +happened to me when I started coding as well. This is why I wanted Discordeno's implementation to help devs avoid the +issues I had. It prevents anyone from having this as a potential issue. Another issue with EventEmitter is trying to +update the code in those functions without having to deal with headaches left and right of removing and adding +listeners. You don't need to worry about binding or not binding events. They are just pure functions + +In Discordeno, this is extremely simple; you just simply give it the new event handlers. For example: + +```typescript +bot.events.guildCreate = newGuildCreateEventHandler +``` + +## Why Do You Have A Class for Collection If Classes Are Bad? + +The Collection class is an exception in the library where a class was allowed. This is because Collection extends Map. +The Map class is provided by JavaScript itself and is extremely fast. You can perform millions of operations a second +with a Map. Maps are too useful to avoid and don't have downsides like EventEmitters do. The Collection class simply +adds on other functionality that Discordeno users felt they needed. Although I am against using classes whenever +possible, I am also a big supporter of providing the best developer experience. + +## Why Are there no options in Discordeno? + +Discordeno is not a library that handles code in the exact way every person wants it to. It is opinionated. Discordeno +defaults to the Discord recommended options or the best options for majority of developers needs. For example, there is +no option of fetching all members startup. This is a practice that Discord does not recommend or want users doing. By +default, we don't support stuff like this. In Discordeno, we follow Discords recommended solution and it just works +internally. The End! No fuss! No Muss! Just good stuff! + +Now, I understand that there are times when it's necessary to be able to customize this and fetch them all. If you are +advanced enough to need these options, you should be able to simply do it yourself. For most users, this is just an +unnecessary option. The main module should remain minimalistic and easy to use for 99% of users. + +## Why Do I See errors Like "MISSING_VIEW_CHANNEL" or "BOTS_HIGHEST_ROLE_TOO_LOW"? + +Discordeno is the only library(that I have used), that has built in permission handling. A lot of bots get automatically +banned by Discord because they forget to handle permissions. When bots don't check permissions and continue to send +requests to the API, this leads to bots being banned. I have tried to request adding this feature into libraries but +they were reluctant to do so because it would require the devs to maintain the library whenever an update was made by +Discord. + +Discordeno provides you specific keywords that you can use to send a clean response to the end user of your choosing. I +have even seen some bots have hundreds of thousands of Missing Permission or Missing Access errors because libraries +don't handle it. IMO, this is a crucial part of any good library as much as it is to handle rate limiting. + +```typescript +import { + Bot, + Errors, + Message, +} from 'https://deno.land/x/discordeno@16.0.0/mod.ts' + +export function handleCommandError(bot: Bot, message: Message, type: Errors) { + switch (type) { + case Errors.MISSING_MANAGE_NICKNAMES: + return bot.helpers.sendMessage(message.channelId, { + content: + "The bot does not have the necessary permission to manage/edit other user's nicknames. Grant the **MANAGE_NICKNAME** permission to the bot and try again.", + }) + case Errors.MISSING_MANAGE_ROLES: + // Note: i18n is not part of the library. This is just an example of how you could use i18n for custom error responses. + return bot.helpers.sendMessage(message.channelId, { + content: i18n.translate(type), + }) + } +} +``` diff --git a/website/old_docs/general/frequently-asked-questions.md b/website/old_docs/general/frequently-asked-questions.md new file mode 100644 index 000000000..791e6a479 --- /dev/null +++ b/website/old_docs/general/frequently-asked-questions.md @@ -0,0 +1,107 @@ +--- +sidebar_position: 5 +--- + +# Frequently Asked Questions + +## Does Discordeno Support TypeScript? + +TypeScript is supported to the highest standard by Discordeno. TypeScript is included in Discordeno since Deno supports it. This implies that before using TypeScript, you do not need to compile it. But this isn't the main reason Discordeno is the ideal library for TypeScript programmers. I was experimenting with a lot of various things when I created this library, and automatic typings was one of them. + +When I utilised other libraries, I frequently observed incorrect or troublesome typings. This is so that TypeScript won't alert the library developers because most of the Discord API typings aren't utilised by the libraries themselves. + +It is quite unlikely that these typings would become wrong or outdated as a result of minor errors like forgetting to update typings because Discordeno utilises them as part of the rest process. Libraries occasionally add a property without also adding it to their typings. Because of this, TypeScript developers cannot use it, only JavaScript developers can. Typings are crucial for TypeScript developers. Typings are treated as a component of the code by Discordeno! A breaking change in typings is a breaking change for the library! + +## How Stable Is Discordeno? + +Stability is one of the main problems with practically every library (I have used). None of the libraries showed TypeScript developers the love and care that they deserve. Because breaking changes to typings occasionally occurred without producing a MAJOR bump, TypeScript projects would occasionally fail. As a result, production TypeScript bots would fail. At times, I was the only one keeping the typings up to date for that library. Some libraries that were older than 1.0 didn't even have a stable branch or version, so I didn't have to worry about them undergoing breaking changes. + +The finest stability for TypeScript developers is one of my basic goals for this library. +No matter how little, a change that impacts the public API qualifies as a breaking change. I don't care whether we reach version 500. As a library maintainer, you should never be scared to bump a MAJOR because it just involves a tiny modification or a type change because doing so will ruin the end user's experience. + +## Why Doesn't Discordeno Use Classes or EventEmitter? + +This is a design decision for the library itself. You can still use class on your bot if you want. In fact, I hope someone +makes a framework/templates for this lib one day using classes so that devs have a choice on which style they prefer. +Without trying to write an entire thesis statement on the reasons why I avoided Classes in this library, I will just link to +the best resources that I believe help explain it. + +- [Really good article](https://dannyfritz.wordpress.com/2014/10/11/class-free-object-oriented-programming/) +- [Lecture by one of the developers who makes JavaScript](https://www.youtube.com/watch?v=PSGEjv3Tqo0) + +In regards to EventEmitter, I believe a functional event API was a much better choice. EventEmitter at its core, is simply a set of functions that run when a certain event is emitted. In Discordeno, that function is executed instead of emitting some event to trigger it. + +```typescript +// EventEmitter Example +EventEmitter.emit('guildCreate', guild) +// Discordeno Example +bot.events.guildCreate?.(bot, guild) +``` + +There isn't really any difference especially for users when they use it. One bad thing about EventEmitter is that if +misused it can easily cause memory leaks. It is very easy to open yourself up to these memory leak issues. It has +happened to me when I started coding as well. This is why I wanted Discordeno's implementation to help devs avoid the +issues I had. It prevents anyone from having this as a potential issue. Another issue with EventEmitter is trying to +update the code in those functions without having to deal with headaches left and right of removing and adding +listeners. You don't need to worry about binding or not binding events. They are just pure functions + +In Discordeno, this is extremely simple; you just simply give it the new event handlers. For example: + +```typescript +bot.events.guildCreate = newGuildCreateEventHandler +``` + +## Why Do You Have A Class for Collection If Classes Are Bad? + +The Collection class is an exception in the library where a class was allowed. This is because Collection extends Map. +The Map class is provided by JavaScript itself and is extremely fast. You can perform millions of operations a second +with a Map. Maps are too useful to avoid and don't have downsides like EventEmitters do. The Collection class simply +adds on other functionality that Discordeno users felt they needed. Although I am against using classes whenever +possible, I am also a big supporter of providing the best developer experience. + +## Why Are there no options in Discordeno? + +Discordeno is not a library that handles code in the exact way every person wants it to. It is opinionated. Discordeno +defaults to the Discord recommended options or the best options for majority of developers needs. For example, there is +no option of fetching all members startup. This is a practice that Discord does not recommend or want users doing. By +default, we don't support stuff like this. In Discordeno, we follow Discords recommended solution and it just works +internally. The End! No fuss! No Muss! Just good stuff! + +Now, I understand that there are times when it's necessary to be able to customize this and fetch them all. If you are +advanced enough to need these options, you should be able to simply do it yourself. For most users, this is just an +unnecessary option. The main module should remain minimalistic and easy to use for 99% of users. + +## Why Do I See errors Like "MISSING_VIEW_CHANNEL" or "BOTS_HIGHEST_ROLE_TOO_LOW"? + +Discordeno is the only library(that I have used), that has built in permission handling. A lot of bots get automatically +banned by Discord because they forget to handle permissions. When bots don't check permissions and continue to send +requests to the API, this leads to bots being banned. I have tried to request adding this feature into libraries but +they were reluctant to do so because it would require the devs to maintain the library whenever an update was made by +Discord. + +Discordeno provides you specific keywords that you can use to send a clean response to the end user of your choosing. I +have even seen some bots have hundreds of thousands of Missing Permission or Missing Access errors because libraries +don't handle it. IMO, this is a crucial part of any good library as much as it is to handle rate limiting. + +```typescript +import { + Bot, + Errors, + Message, +} from 'https://deno.land/x/discordeno@16.0.0/mod.ts' + +export function handleCommandError(bot: Bot, message: Message, type: Errors) { + switch (type) { + case Errors.MISSING_MANAGE_NICKNAMES: + return bot.helpers.sendMessage(message.channelId, { + content: + "The bot does not have the necessary permission to manage/edit other user's nicknames. Grant the **MANAGE_NICKNAME** permission to the bot and try again.", + }) + case Errors.MISSING_MANAGE_ROLES: + // Note: i18n is not part of the library. This is just an example of how you could use i18n for custom error responses. + return bot.helpers.sendMessage(message.channelId, { + content: i18n.translate(type), + }) + } +} +``` diff --git a/website/old_docs/general/getting-started.md b/website/old_docs/general/getting-started.md new file mode 100644 index 000000000..09445ac98 --- /dev/null +++ b/website/old_docs/general/getting-started.md @@ -0,0 +1,80 @@ +--- +sidebar_position: 3 +--- + +# Getting Started + +Discordeno aims for a simple, easy and stress-free interaction with the Discord API. Always supporting the latest +version to ensure stability, consistency and the best developer experience. This guide serves as the purpose for +introducing Discordeno to developers. + +## Requirements + +- **Deno 1.0** or higher + +## Creating your First Discord Bot Application + +Plenty of guides are available on how to create a Discord Bot Application. + +1. [Creating an Application](https://discord.com/developers/applications) on the Developer Portal, name something cool + and pick a sweet icon! +2. After creating an application. Save the **Client ID.** Thats the unique identifier for a Discord Bot. +3. Now, go and create a bot by clicking the **Bot** tab. You will see a **Token** section and thats the Discord Bot's + token. **Make sure you don't share that token with anyone!!!** +4. Invite the bot to the server, you can use the + **[Discord Permissions Calculator](https://discordapi.com/permissions.html#0)** for creating the invite link with + custom permissions. By default, `0` means no permissions and `8` means Administrator. + +Now you've created an Application but it will need some code in order for it to be online. Thats when Discordeno comes +in handy! + +> Make sure you store your tokens in a file that is NOT deployed by adding it to the .gitignore file. **Don't share your +> bot token with anybody.** + +## Installation + +You can install Discordeno by importing: + +```ts +import { startBot } from 'https://deno.land/x/discordeno@16.0.0/mod.ts' +``` + +## Example Usage + +Starting with Discordeno is very simple, you can start from scratch without any templates/frameworks: Add this snippet +of code into a new TypeScript file: + +```ts +import { + createBot, + Intents, + startBot, +} from 'https://deno.land/x/discordeno/mod.ts' + +startBot( + createBot({ + token: 'BOT TOKEN', + intents: Intents.Guilds | Intents.GuildMessages, + events: { + ready() { + console.log('Successfully connected to gateway') + }, + messageCreate(bot, message) { + if (message.content === '!ping') { + bot.helpers.sendMessage(message.channelId, { + content: 'Pong using Discordeno!', + }) + } + }, + }, + }), +) +``` + +## Tutorials + +Below you will find youtube playlists that display channels using Discordeno for their tutorials. + +- [Making a Discord bot with Deno and Discordeno](https://web-mystery.com/articles/making-discord-bot-deno-and-discordeno) +- [Running a Discord bot written using Deno in Docker](https://web-mystery.com/articles/running-discord-bot-written-deno-docker) +- [Discordeno Bot Tutorials (YouTube)](https://youtu.be/rIph9-BGsuQ) diff --git a/website/old_docs/general/migrating.md b/website/old_docs/general/migrating.md new file mode 100644 index 000000000..3639419ae --- /dev/null +++ b/website/old_docs/general/migrating.md @@ -0,0 +1,487 @@ +--- +sidebar_position: 4 +--- + +# Migrating + +## Migrating from Discord.js + +This migration guide is not intended to discredit Discord.js authors/maintainers or Discord.js itself. In fact, +Discord.js is the most popular Node.js library, admired and praised by a lot of JavaScript developers. + +## Finding an Open-Source Discord Bot + +For the purposes of this guide, I wanted to find a moderation bot that is totally open source to show an example of how +to convert the bot to Discordeno. Trying to find one was not easy as most bot's were not using the latest Discord.JS +version 12. Trying to find one that was using TypeScript made it even more difficult. My next best solution was to find +a moderation bot that was recently updated(showing it is maintained or recently built). The best one I could find was +[Zodiac Bot](https://github.com/Nukestye/Zodiac). + +For the purposes of this guide, I will be using the current +[latest commit](https://github.com/Nukestye/Zodiac/tree/213891a38af1b7ecbd068b661ef9062ab58cc818) + +## Preparations + +- First, create a Discordeno Bot using the [Generator Template](https://github.com/discordeno/template) I will name it + Zodiac. + +- Then `git clone https://github.com/Skillz4Killz/Zodiac.git` + +Now that I had the repository cloned, I could begin. Note that although the bot we are converting is built in +JavaScript, I converted all code to TypeScript in this Guide as Discordeno is designed to be the best lib for TypeScript +developers. + +Time to get started! + +## Converting main.js (index file) + +The first thing is to convert the `main.js` file which would be the app.js or index.js file. This is the file that is +run to start your bot. In this case, the bot developer chose `main.js`. In Deno, the initial file is named `mod.ts` so +we can go ahead and opt for the Deno pattern. Note: there is already a `mod.ts` file created and prebuilt entirely using +the Generator. + +Current Discord.JS Code: + +```js +/* Keeping this to shoutout/credit the original author <3 + * @author: nukestye + */ + +const config = require('./config.json') +const fs = require('fs') +const log = console.log + +// Setting up the way to get commands +const { CommandoClient } = require('discord.js-commando') +const path = require('path') + +// reading events +fs.readdir('./src/events/', (err, files) => { + if (err) return console.error(err) + files.forEach(file => { + const eventFunction = require(`./src/events/${file}`) + if (eventFunction.disabled) return + const event = eventFunction.event || file.split('.')[0] + const emitter = + (typeof eventFunction.emitter === 'string' + ? client[eventFunction.emitter] + : eventFunction.emitter) || client + const { once } = eventFunction + try { + emitter[once ? 'once' : 'on'](event, (...args) => + eventFunction.run(...args), + ) + } catch (error) { + console.error(error.stack) + } + }) +}) + +const client = (global.client = new CommandoClient({ + commandPrefix: `${config.prefix}`, + owner: `${config.owner}`, + invite: `${config.discord}`, + unknownCommandResponse: false, +})) + +// Registing the commands +client.registry + .registerDefaultTypes() + // The different fields for cmds + .registerGroups([ + ['mod', 'Moderation Commands'], + ['public', 'Public Commands'], + ]) + .registerDefaultGroups() + // Basic cmds can be disabled like {"cmd: false"} + .registerDefaultCommands() + // commands in "/src/commands" will be counted + .registerCommandsIn(path.join(__dirname, '/src/commands')) + +// list of activities that the bot goes through +const activityArray = [`${config.prefix}help | `] +// Bot lanuch code +client.once('ready', () => { + log(`Logged in as ${client.user.tag} in ${client.guilds.size} guild(s)!`) + setInterval(() => { + const index = Math.floor(Math.random() * activityArray.length) // generates a random number between 1 and the length of the activities array list + client.user.setActivity(activityArray[index], { + type: 'PLAYING', + }) // sets bot"s activities to one of the phrases in the arraylist. + }, 5000) // updates every 10000ms = 10s +}) +// If an error print it out +client.on('error', console.error) + +// Login in using the token in config +client.login(config.env.TOKEN) +``` + +Discordeno Version: + +```ts +import { botCache, Intents } from './deps.ts' +import { configs } from './configs.ts' +import { importDirectory } from './src/utils/helpers.ts' +import { loadLanguages } from './src/utils/i18next.ts' + +console.info( + 'Beginning Bot Startup Process. This can take a little bit depending on your system. Loading now...', +) + +// Always require these files be processed before anything else +await Promise.all( + ['./src/customizations/structures'].map(path => + importDirectory(Deno.realPathSync(path)), + ), +) + +// Forces deno to read all the files which will fill the commands/inhibitors cache etc. +await Promise.all( + [ + './src/commands', + './src/inhibitors', + './src/events', + './src/arguments', + './src/monitors', + './src/tasks', + './src/permissionLevels', + './src/events', + ].map(path => importDirectory(Deno.realPathSync(path))), +) + +// Loads languages +await loadLanguages() +await import('./src/database/database.ts') + +startBot({ + token: configs.token, + // Pick the intents you wish to have for your bot. + // For instance, to work with guild message reactions, you will have to pass the Intents.GUILD_MESSAGE_REACTIONS intent to the array. + intents: Intents.Guilds | Intents.GuildMessages, + // These are all your event handler functions. Imported from the events folder + events: botCache.events, +}) +``` + +Something we haven't converted yet from the `main.js` files is the event listeners. To do that, we will open up the +events folder and find the corresponding event or create it if necessary. In this case, we have the `ready` event and +there is already a `ready.ts` file. We can just use that. + +In our `ready.ts` file we can add the `ready` event listener. + +```ts +import { + ActivityTypes, + botCache, + cache, + chooseRandom, + editBotStatus, + StatusTypes, +} from '../../deps.ts' +import { registerTasks } from './../utils/taskHelper.ts' + +botCache.events.ready = function () { + editBotStatus( + StatusTypes.DoNotDisturb, + 'Discordeno Best Lib', + ActivityTypes.Game, + ) + + console.log(`Loaded ${botCache.arguments.size} Argument(s)`) + console.log(`Loaded ${botCache.commands.size} Command(s)`) + console.log(`Loaded ${Object.keys(botCache.events).length} Event(s)`) + console.log(`Loaded ${botCache.inhibitors.size} Inhibitor(s)`) + console.log(`Loaded ${botCache.monitors.size} Monitor(s)`) + console.log(`Loaded ${botCache.tasks.size} Task(s)`) + + registerTasks() + + console.log( + `[READY] Bot is online and ready in ${cache.guilds.size} guild(s)!`, + ) + + // list of activities that the bot goes through + const activityArray = [`${configs.prefix}help | `] + setInterval(() => { + const randomActivity = + activityArray[Math.floor(Math.random() * activityArray.length)] + + editBotStatus(botCache, { + activities: [ + { + name: randomActivity, + type: ActivityTypes.Game, + createdAt: Date.now(), + }, + ], + status: 'online', + }) + }, 5000) +} +``` + +To understand this code, we are setting a function to be run when the bot is `ready`. Then the bot will edit the bot's +status every 5 seconds. Notice that you also have beautiful enums provided that prevents you from making any +typos/mistakes. + +We have now converted the entire `main.js` file, in a matter of seconds. The Discordeno official generator took care of +the majority of workload and we just modified the `ready.ts` file. + +`Note:` I did remove some generally well known "bad practices" such as global vars and such. Overall, you will see the +functionality of the project will not change as we progress through this guide. + +## Converting Commands + +The first command in the commands folder is the `addRole` command. + +This is the code from the bot: + +```ts +// Getting the 'Command' features from Commando +const { Command } = require('discord.js-commando') + +// Code for the command +module.exports = class addRoleCommand extends Command { + constructor(client) { + super(client, { + // name of the command, must be in lowercase + name: 'addrole', + // other ways to call the command, must be in lowercase + aliases: ['role'], + // command group its part of + group: 'mod', + // name within the command group, must be in lowercase + memberName: 'addrole', + // Is the description used for 'help' command + description: 'Adds mentioned role to mentioned user.', + // Prevents it from being used in dms + guildOnly: true, + // Permissions, list found here > `discord.js.org/#/docs/main/11.5.1/class/Permissions?scrollTo=s-FLAGS` + clientPermissions: ['ADMINISTRATOR', 'MANAGE_ROLES'], + userPermissions: ['MANAGE_ROLES'], + // Prevents anyone other than owner to use the command + ownerOnly: false, + }) + } + + // Run code goes here + run(message) { + const user = message.mentions.members.first() + const roleToAdd = message.mentions.roles.first() + + // checking to see if the user has the role or not + if (!user.roles.find(r => r.name === roleToAdd.name)) { + user.addRole(roleToAdd) + message.channel + .send(`${user} has been given the role: ${roleToAdd.name}`) + .then(msg => { + msg.delete(5000) + }) + } else { + message.channel.send(`${user} already has the role: ${roleToAdd.name}`) + } + + // console.error(user, roleToAdd, message.member.roles.find(r => r.name === roleToAdd)); + } +} +``` + +This is how to do it with Discordeno: + +```ts +import { createCommand } from './../../utils/helpers.ts' + +createCommand({ + name: 'role', + // Oher ways to call the command + aliases: ['addrole'], + // Is the description used for 'help' command + description: 'Adds mentioned role to mentioned user.', + // Prevents it from being used in dms + guildOnly: true, + botServerPermissions: ['ADMINISTRATOR', 'MANAGE_ROLES'], + userServerPermissions: ['MANAGE_ROLES'], + arguments: [ + { name: 'member', type: 'member' }, + { name: 'role', type: 'role' }, + ], + execute: (bot, message, args) => { + // checking to see if the user has the role or not + if (!args.member.roles.includes(args.role.id)) { + bot.helpers.addRole(message.guildId, args.member.id, args.role.id) + bot.helpers.sendMessage(message.channelId, { + content: `${args.member.mention} has been given the role: ${args.role.name}`, + }) + } else { + bot.helpers.sendMessage(message.channelId, { + content: `${args.member.mention} already has the role: ${args.role.name}`, + }) + } + }, +}) +``` + +Awesome, that is a full command converted from Discord.JS to Discordeno. See how easy it is! Let's convert one more +command to see how to really take full advantage of Discordeno template and have something amazing. + +Discord.JS Kick Command Version + +```js +// Getting the 'Command' features from Commando +const { Command } = require('discord.js-commando') +const { RichEmbed } = require('discord.js') +const chalk = require('chalk') +const log = console.log + +// Code for the command +module.exports = class kickCommand extends Command { + constructor(client) { + super(client, { + // name of the command, must be in lowercase + name: 'kick', + // other ways to call the command, must be in lowercase + aliases: ['boot', 'tempban'], + // command group its part of + group: 'mod', + // name within the command group, must be in lowercase + memberName: 'kick', + // Is the description used for 'help' command + description: 'Kick command.', + // adds cooldowns to the command + throttling: { + // usages in certain time x + usages: 1, + // the cooldown + duration: 10, + }, + // Prevents it from being used in dms + guildOnly: true, + // Permissions, list found here > `discord.js.org/#/docs/main/11.5.1/class/Permissions?scrollTo=s-FLAGS` + clientPermissions: ['ADMINISTRATOR'], + userPermissions: ['KICK_MEMBERS'], + // Prevents anyone other than owner to use the command + ownerOnly: false, + }) + } + + // Run code goes here + run(message) { + const messageArry = message.content.split(' ') + const args = messageArry.slice(1) + + const kUser = message.guild.member( + message.mentions.users.first() || message.guild.get(args[0]), + ) + if (!kUser) return message.channel.send('User cannot be found!') + const kreason = args.join(' ').slice(22) + + // setting up the embed for report/log + const kickEmbed = new RichEmbed() + .setDescription(`Report: ${kUser} Kick`) + .addField('Reason >', `${kreason}`) + .addField('Time', message.createdAt) + + const reportchannel = message.guild.channels.find('name', 'report') + if (!reportchannel) { + return message.channel.send('*`Report channel cannot be found!`*') + } + + // Delete the message command + // eslint-disable-next-line camelcase + message.delete().catch(O_o => {}) + // Kick the user with reason + message.guild.member(kUser).kick(kreason) + // sends the kick report into log/report + reportchannel.send(kickEmbed) + // Logs the kick into the terminal + log(chalk.red('KICK', chalk.underline.bgBlue(kUser) + '!')) + } +} +``` + +Discordeno Version + +```ts +import { createCommand } from './../../utils/helpers.ts' + +createCommand({ + name: `kick`, + aliases: ['boot', 'tempban'], + description: 'Kick command.', + // adds cooldowns to the command + cooldown: { + // usages in certain duration of seconds below + allowedUses: 1, + // the cooldown + seconds: 10, + }, + // Prevents it from being used in dms + guildOnly: true, + botServerPermissions: ['ADMINISTRATOR'], + userServerPermissions: ['KICK_MEMBERS'], + arguments: [ + { + name: 'member', + type: 'member', + missing: function (message) { + message.reply(`User cannot be found.`) + }, + // By default this is true but for the purpose of the guide so you can see this exists. + required: true, + }, + { + name: 'reason', + // The leftover string provided by the user that was not used by previous args. + type: '...string', + defaultValue: 'No reason provided.', + // It is silly to lowercase this but for the purpose of the guide you can see that this is also available to you. + lowercase: true, + }, + ], + execute: function (bot, message, args: KickArgs) { + // setting up the embed for report/log + const embed = new Embed() + .setDescription(`Report: ${args.member.mention} Kick`) + .addField('Reason >', args.reason) + .addField('Time', message.timestamp.toString()) + + const reportchannel = message.guild?.channels.find( + channel => channel.name === 'report', + ) + if (!reportchannel) { + return bot.helpers.sendMessage(message.channelId, { + content: '*`Report channel cannot be found!`*', + }) + } + + // Delete the message command + bot.helpers.deleteMessage(message.channelId, { + content: 'Remove kick command trigger.', + }) + // Kick the user with reason + bot.helpers.kickMember(message.guildId, args.member.id, args.reason) + // sends the kick report into log/report + bot.helpers.sendMessage(message.channelId, { embeds: [embed] }) + }, +}) + +interface KickArgs { + member: Member + reason: string +} +``` + +Let's take a minute and explain the differences here. The first thing you will probably notice is different is the +`arguments` property. Discordeno provides the `arguments` property because it provides argument +handling/parsing/validating internally. You don't need to be splitting the message content or going through and +validating it yourself. All you do is tell Discordeno that you want a member and a reason. It will do the magic and hard +work to get you that data before you even run the command. You just do `args.member` and you have access to the full +member object. There are a lot more powerful aspects to Discordeno like arguments. Keep diving in and you will find all +the wonderful tools available to give you the best developer experience possible. + +### Need More Examples/Help + +If you still need more help converting other aspects of your bot please contact me at +[Discord](https://discord.com/invite/5vBgXk3UcZ). I will continue adding more examples to this guide as more people +request them. diff --git a/website/old_docs/getting-started.md b/website/old_docs/getting-started.md new file mode 100644 index 000000000..09445ac98 --- /dev/null +++ b/website/old_docs/getting-started.md @@ -0,0 +1,80 @@ +--- +sidebar_position: 3 +--- + +# Getting Started + +Discordeno aims for a simple, easy and stress-free interaction with the Discord API. Always supporting the latest +version to ensure stability, consistency and the best developer experience. This guide serves as the purpose for +introducing Discordeno to developers. + +## Requirements + +- **Deno 1.0** or higher + +## Creating your First Discord Bot Application + +Plenty of guides are available on how to create a Discord Bot Application. + +1. [Creating an Application](https://discord.com/developers/applications) on the Developer Portal, name something cool + and pick a sweet icon! +2. After creating an application. Save the **Client ID.** Thats the unique identifier for a Discord Bot. +3. Now, go and create a bot by clicking the **Bot** tab. You will see a **Token** section and thats the Discord Bot's + token. **Make sure you don't share that token with anyone!!!** +4. Invite the bot to the server, you can use the + **[Discord Permissions Calculator](https://discordapi.com/permissions.html#0)** for creating the invite link with + custom permissions. By default, `0` means no permissions and `8` means Administrator. + +Now you've created an Application but it will need some code in order for it to be online. Thats when Discordeno comes +in handy! + +> Make sure you store your tokens in a file that is NOT deployed by adding it to the .gitignore file. **Don't share your +> bot token with anybody.** + +## Installation + +You can install Discordeno by importing: + +```ts +import { startBot } from 'https://deno.land/x/discordeno@16.0.0/mod.ts' +``` + +## Example Usage + +Starting with Discordeno is very simple, you can start from scratch without any templates/frameworks: Add this snippet +of code into a new TypeScript file: + +```ts +import { + createBot, + Intents, + startBot, +} from 'https://deno.land/x/discordeno/mod.ts' + +startBot( + createBot({ + token: 'BOT TOKEN', + intents: Intents.Guilds | Intents.GuildMessages, + events: { + ready() { + console.log('Successfully connected to gateway') + }, + messageCreate(bot, message) { + if (message.content === '!ping') { + bot.helpers.sendMessage(message.channelId, { + content: 'Pong using Discordeno!', + }) + } + }, + }, + }), +) +``` + +## Tutorials + +Below you will find youtube playlists that display channels using Discordeno for their tutorials. + +- [Making a Discord bot with Deno and Discordeno](https://web-mystery.com/articles/making-discord-bot-deno-and-discordeno) +- [Running a Discord bot written using Deno in Docker](https://web-mystery.com/articles/running-discord-bot-written-deno-docker) +- [Discordeno Bot Tutorials (YouTube)](https://youtu.be/rIph9-BGsuQ) diff --git a/website/old_docs/intro.md b/website/old_docs/intro.md new file mode 100644 index 000000000..1b5d4cef2 --- /dev/null +++ b/website/old_docs/intro.md @@ -0,0 +1,23 @@ +--- +sidebar_position: 1 +--- + +# Discordeno + +> Discord API library for [Deno](https://deno.land) + +- [Documentation](https://doc.deno.land/https/deno.land/x/discordeno/mod.ts) +- [Discord](https://discord.gg/ddeno) + +## Features + +- **Secure & stable**: Discordeno is secure and stable. One of the greatest issues with almost every library is + stability; types are outdated, less (or minimal) parity with the API, core maintainers have quit or no longer actively + maintain the library, and whatnot. Discordeno, on the other hand, is actively maintained to ensure great performance + and convenience. Moreover, it internally checks all missing permissions before forwarding a request to the Discord API + so that the client does not get globally-banned by Discord. +- **Simple, Efficient, & Lightweight**: Discordeno is simplistic, easy-to-use, versatile while being efficient and + lightweight. +- [**Functional API**](https://en.wikipedia.org/wiki/Functional_programming): Functional API ensures an overall concise + yet performant code while removing the difficulties of extending built-in classes and inheritance. + [Learn more about class-free JavaScript](https://dannyfritz.wordpress.com/2014/10/11/class-free-object-oriented-programming/) diff --git a/website/old_docs/migrating.md b/website/old_docs/migrating.md new file mode 100644 index 000000000..3639419ae --- /dev/null +++ b/website/old_docs/migrating.md @@ -0,0 +1,487 @@ +--- +sidebar_position: 4 +--- + +# Migrating + +## Migrating from Discord.js + +This migration guide is not intended to discredit Discord.js authors/maintainers or Discord.js itself. In fact, +Discord.js is the most popular Node.js library, admired and praised by a lot of JavaScript developers. + +## Finding an Open-Source Discord Bot + +For the purposes of this guide, I wanted to find a moderation bot that is totally open source to show an example of how +to convert the bot to Discordeno. Trying to find one was not easy as most bot's were not using the latest Discord.JS +version 12. Trying to find one that was using TypeScript made it even more difficult. My next best solution was to find +a moderation bot that was recently updated(showing it is maintained or recently built). The best one I could find was +[Zodiac Bot](https://github.com/Nukestye/Zodiac). + +For the purposes of this guide, I will be using the current +[latest commit](https://github.com/Nukestye/Zodiac/tree/213891a38af1b7ecbd068b661ef9062ab58cc818) + +## Preparations + +- First, create a Discordeno Bot using the [Generator Template](https://github.com/discordeno/template) I will name it + Zodiac. + +- Then `git clone https://github.com/Skillz4Killz/Zodiac.git` + +Now that I had the repository cloned, I could begin. Note that although the bot we are converting is built in +JavaScript, I converted all code to TypeScript in this Guide as Discordeno is designed to be the best lib for TypeScript +developers. + +Time to get started! + +## Converting main.js (index file) + +The first thing is to convert the `main.js` file which would be the app.js or index.js file. This is the file that is +run to start your bot. In this case, the bot developer chose `main.js`. In Deno, the initial file is named `mod.ts` so +we can go ahead and opt for the Deno pattern. Note: there is already a `mod.ts` file created and prebuilt entirely using +the Generator. + +Current Discord.JS Code: + +```js +/* Keeping this to shoutout/credit the original author <3 + * @author: nukestye + */ + +const config = require('./config.json') +const fs = require('fs') +const log = console.log + +// Setting up the way to get commands +const { CommandoClient } = require('discord.js-commando') +const path = require('path') + +// reading events +fs.readdir('./src/events/', (err, files) => { + if (err) return console.error(err) + files.forEach(file => { + const eventFunction = require(`./src/events/${file}`) + if (eventFunction.disabled) return + const event = eventFunction.event || file.split('.')[0] + const emitter = + (typeof eventFunction.emitter === 'string' + ? client[eventFunction.emitter] + : eventFunction.emitter) || client + const { once } = eventFunction + try { + emitter[once ? 'once' : 'on'](event, (...args) => + eventFunction.run(...args), + ) + } catch (error) { + console.error(error.stack) + } + }) +}) + +const client = (global.client = new CommandoClient({ + commandPrefix: `${config.prefix}`, + owner: `${config.owner}`, + invite: `${config.discord}`, + unknownCommandResponse: false, +})) + +// Registing the commands +client.registry + .registerDefaultTypes() + // The different fields for cmds + .registerGroups([ + ['mod', 'Moderation Commands'], + ['public', 'Public Commands'], + ]) + .registerDefaultGroups() + // Basic cmds can be disabled like {"cmd: false"} + .registerDefaultCommands() + // commands in "/src/commands" will be counted + .registerCommandsIn(path.join(__dirname, '/src/commands')) + +// list of activities that the bot goes through +const activityArray = [`${config.prefix}help | `] +// Bot lanuch code +client.once('ready', () => { + log(`Logged in as ${client.user.tag} in ${client.guilds.size} guild(s)!`) + setInterval(() => { + const index = Math.floor(Math.random() * activityArray.length) // generates a random number between 1 and the length of the activities array list + client.user.setActivity(activityArray[index], { + type: 'PLAYING', + }) // sets bot"s activities to one of the phrases in the arraylist. + }, 5000) // updates every 10000ms = 10s +}) +// If an error print it out +client.on('error', console.error) + +// Login in using the token in config +client.login(config.env.TOKEN) +``` + +Discordeno Version: + +```ts +import { botCache, Intents } from './deps.ts' +import { configs } from './configs.ts' +import { importDirectory } from './src/utils/helpers.ts' +import { loadLanguages } from './src/utils/i18next.ts' + +console.info( + 'Beginning Bot Startup Process. This can take a little bit depending on your system. Loading now...', +) + +// Always require these files be processed before anything else +await Promise.all( + ['./src/customizations/structures'].map(path => + importDirectory(Deno.realPathSync(path)), + ), +) + +// Forces deno to read all the files which will fill the commands/inhibitors cache etc. +await Promise.all( + [ + './src/commands', + './src/inhibitors', + './src/events', + './src/arguments', + './src/monitors', + './src/tasks', + './src/permissionLevels', + './src/events', + ].map(path => importDirectory(Deno.realPathSync(path))), +) + +// Loads languages +await loadLanguages() +await import('./src/database/database.ts') + +startBot({ + token: configs.token, + // Pick the intents you wish to have for your bot. + // For instance, to work with guild message reactions, you will have to pass the Intents.GUILD_MESSAGE_REACTIONS intent to the array. + intents: Intents.Guilds | Intents.GuildMessages, + // These are all your event handler functions. Imported from the events folder + events: botCache.events, +}) +``` + +Something we haven't converted yet from the `main.js` files is the event listeners. To do that, we will open up the +events folder and find the corresponding event or create it if necessary. In this case, we have the `ready` event and +there is already a `ready.ts` file. We can just use that. + +In our `ready.ts` file we can add the `ready` event listener. + +```ts +import { + ActivityTypes, + botCache, + cache, + chooseRandom, + editBotStatus, + StatusTypes, +} from '../../deps.ts' +import { registerTasks } from './../utils/taskHelper.ts' + +botCache.events.ready = function () { + editBotStatus( + StatusTypes.DoNotDisturb, + 'Discordeno Best Lib', + ActivityTypes.Game, + ) + + console.log(`Loaded ${botCache.arguments.size} Argument(s)`) + console.log(`Loaded ${botCache.commands.size} Command(s)`) + console.log(`Loaded ${Object.keys(botCache.events).length} Event(s)`) + console.log(`Loaded ${botCache.inhibitors.size} Inhibitor(s)`) + console.log(`Loaded ${botCache.monitors.size} Monitor(s)`) + console.log(`Loaded ${botCache.tasks.size} Task(s)`) + + registerTasks() + + console.log( + `[READY] Bot is online and ready in ${cache.guilds.size} guild(s)!`, + ) + + // list of activities that the bot goes through + const activityArray = [`${configs.prefix}help | `] + setInterval(() => { + const randomActivity = + activityArray[Math.floor(Math.random() * activityArray.length)] + + editBotStatus(botCache, { + activities: [ + { + name: randomActivity, + type: ActivityTypes.Game, + createdAt: Date.now(), + }, + ], + status: 'online', + }) + }, 5000) +} +``` + +To understand this code, we are setting a function to be run when the bot is `ready`. Then the bot will edit the bot's +status every 5 seconds. Notice that you also have beautiful enums provided that prevents you from making any +typos/mistakes. + +We have now converted the entire `main.js` file, in a matter of seconds. The Discordeno official generator took care of +the majority of workload and we just modified the `ready.ts` file. + +`Note:` I did remove some generally well known "bad practices" such as global vars and such. Overall, you will see the +functionality of the project will not change as we progress through this guide. + +## Converting Commands + +The first command in the commands folder is the `addRole` command. + +This is the code from the bot: + +```ts +// Getting the 'Command' features from Commando +const { Command } = require('discord.js-commando') + +// Code for the command +module.exports = class addRoleCommand extends Command { + constructor(client) { + super(client, { + // name of the command, must be in lowercase + name: 'addrole', + // other ways to call the command, must be in lowercase + aliases: ['role'], + // command group its part of + group: 'mod', + // name within the command group, must be in lowercase + memberName: 'addrole', + // Is the description used for 'help' command + description: 'Adds mentioned role to mentioned user.', + // Prevents it from being used in dms + guildOnly: true, + // Permissions, list found here > `discord.js.org/#/docs/main/11.5.1/class/Permissions?scrollTo=s-FLAGS` + clientPermissions: ['ADMINISTRATOR', 'MANAGE_ROLES'], + userPermissions: ['MANAGE_ROLES'], + // Prevents anyone other than owner to use the command + ownerOnly: false, + }) + } + + // Run code goes here + run(message) { + const user = message.mentions.members.first() + const roleToAdd = message.mentions.roles.first() + + // checking to see if the user has the role or not + if (!user.roles.find(r => r.name === roleToAdd.name)) { + user.addRole(roleToAdd) + message.channel + .send(`${user} has been given the role: ${roleToAdd.name}`) + .then(msg => { + msg.delete(5000) + }) + } else { + message.channel.send(`${user} already has the role: ${roleToAdd.name}`) + } + + // console.error(user, roleToAdd, message.member.roles.find(r => r.name === roleToAdd)); + } +} +``` + +This is how to do it with Discordeno: + +```ts +import { createCommand } from './../../utils/helpers.ts' + +createCommand({ + name: 'role', + // Oher ways to call the command + aliases: ['addrole'], + // Is the description used for 'help' command + description: 'Adds mentioned role to mentioned user.', + // Prevents it from being used in dms + guildOnly: true, + botServerPermissions: ['ADMINISTRATOR', 'MANAGE_ROLES'], + userServerPermissions: ['MANAGE_ROLES'], + arguments: [ + { name: 'member', type: 'member' }, + { name: 'role', type: 'role' }, + ], + execute: (bot, message, args) => { + // checking to see if the user has the role or not + if (!args.member.roles.includes(args.role.id)) { + bot.helpers.addRole(message.guildId, args.member.id, args.role.id) + bot.helpers.sendMessage(message.channelId, { + content: `${args.member.mention} has been given the role: ${args.role.name}`, + }) + } else { + bot.helpers.sendMessage(message.channelId, { + content: `${args.member.mention} already has the role: ${args.role.name}`, + }) + } + }, +}) +``` + +Awesome, that is a full command converted from Discord.JS to Discordeno. See how easy it is! Let's convert one more +command to see how to really take full advantage of Discordeno template and have something amazing. + +Discord.JS Kick Command Version + +```js +// Getting the 'Command' features from Commando +const { Command } = require('discord.js-commando') +const { RichEmbed } = require('discord.js') +const chalk = require('chalk') +const log = console.log + +// Code for the command +module.exports = class kickCommand extends Command { + constructor(client) { + super(client, { + // name of the command, must be in lowercase + name: 'kick', + // other ways to call the command, must be in lowercase + aliases: ['boot', 'tempban'], + // command group its part of + group: 'mod', + // name within the command group, must be in lowercase + memberName: 'kick', + // Is the description used for 'help' command + description: 'Kick command.', + // adds cooldowns to the command + throttling: { + // usages in certain time x + usages: 1, + // the cooldown + duration: 10, + }, + // Prevents it from being used in dms + guildOnly: true, + // Permissions, list found here > `discord.js.org/#/docs/main/11.5.1/class/Permissions?scrollTo=s-FLAGS` + clientPermissions: ['ADMINISTRATOR'], + userPermissions: ['KICK_MEMBERS'], + // Prevents anyone other than owner to use the command + ownerOnly: false, + }) + } + + // Run code goes here + run(message) { + const messageArry = message.content.split(' ') + const args = messageArry.slice(1) + + const kUser = message.guild.member( + message.mentions.users.first() || message.guild.get(args[0]), + ) + if (!kUser) return message.channel.send('User cannot be found!') + const kreason = args.join(' ').slice(22) + + // setting up the embed for report/log + const kickEmbed = new RichEmbed() + .setDescription(`Report: ${kUser} Kick`) + .addField('Reason >', `${kreason}`) + .addField('Time', message.createdAt) + + const reportchannel = message.guild.channels.find('name', 'report') + if (!reportchannel) { + return message.channel.send('*`Report channel cannot be found!`*') + } + + // Delete the message command + // eslint-disable-next-line camelcase + message.delete().catch(O_o => {}) + // Kick the user with reason + message.guild.member(kUser).kick(kreason) + // sends the kick report into log/report + reportchannel.send(kickEmbed) + // Logs the kick into the terminal + log(chalk.red('KICK', chalk.underline.bgBlue(kUser) + '!')) + } +} +``` + +Discordeno Version + +```ts +import { createCommand } from './../../utils/helpers.ts' + +createCommand({ + name: `kick`, + aliases: ['boot', 'tempban'], + description: 'Kick command.', + // adds cooldowns to the command + cooldown: { + // usages in certain duration of seconds below + allowedUses: 1, + // the cooldown + seconds: 10, + }, + // Prevents it from being used in dms + guildOnly: true, + botServerPermissions: ['ADMINISTRATOR'], + userServerPermissions: ['KICK_MEMBERS'], + arguments: [ + { + name: 'member', + type: 'member', + missing: function (message) { + message.reply(`User cannot be found.`) + }, + // By default this is true but for the purpose of the guide so you can see this exists. + required: true, + }, + { + name: 'reason', + // The leftover string provided by the user that was not used by previous args. + type: '...string', + defaultValue: 'No reason provided.', + // It is silly to lowercase this but for the purpose of the guide you can see that this is also available to you. + lowercase: true, + }, + ], + execute: function (bot, message, args: KickArgs) { + // setting up the embed for report/log + const embed = new Embed() + .setDescription(`Report: ${args.member.mention} Kick`) + .addField('Reason >', args.reason) + .addField('Time', message.timestamp.toString()) + + const reportchannel = message.guild?.channels.find( + channel => channel.name === 'report', + ) + if (!reportchannel) { + return bot.helpers.sendMessage(message.channelId, { + content: '*`Report channel cannot be found!`*', + }) + } + + // Delete the message command + bot.helpers.deleteMessage(message.channelId, { + content: 'Remove kick command trigger.', + }) + // Kick the user with reason + bot.helpers.kickMember(message.guildId, args.member.id, args.reason) + // sends the kick report into log/report + bot.helpers.sendMessage(message.channelId, { embeds: [embed] }) + }, +}) + +interface KickArgs { + member: Member + reason: string +} +``` + +Let's take a minute and explain the differences here. The first thing you will probably notice is different is the +`arguments` property. Discordeno provides the `arguments` property because it provides argument +handling/parsing/validating internally. You don't need to be splitting the message content or going through and +validating it yourself. All you do is tell Discordeno that you want a member and a reason. It will do the magic and hard +work to get you that data before you even run the command. You just do `args.member` and you have access to the full +member object. There are a lot more powerful aspects to Discordeno like arguments. Keep diving in and you will find all +the wonderful tools available to give you the best developer experience possible. + +### Need More Examples/Help + +If you still need more help converting other aspects of your bot please contact me at +[Discord](https://discord.com/invite/5vBgXk3UcZ). I will continue adding more examples to this guide as more people +request them. diff --git a/website/old_docs/nodejs/CommandHandler/_category_.json b/website/old_docs/nodejs/CommandHandler/_category_.json new file mode 100644 index 000000000..dd4267393 --- /dev/null +++ b/website/old_docs/nodejs/CommandHandler/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Command Handler", + "position": 9 +} diff --git a/website/old_docs/nodejs/CommandHandler/command-manager.md b/website/old_docs/nodejs/CommandHandler/command-manager.md new file mode 100644 index 000000000..354be6a83 --- /dev/null +++ b/website/old_docs/nodejs/CommandHandler/command-manager.md @@ -0,0 +1,92 @@ +--- +sidebar_position: 2 +--- + +# Command Manager + +Currently, you probably have something like this in your code: + +```js +const Discord = require('discordeno.js') +// Ideally you should move to an `.env` file +const config = require('./config.json') + +const bot = Discord.createBot({ + events: { + messageCreate(client, message) { + if (message.content === '!ping') { + client.helpers.sendMessage(message.channelId, { content: 'pong' }) + } + }, + }, + intents: Discord.Intents.Guilds | Discord.Intents.GuildMessages, + token: config.token, +}) +const client = Discord.enableCachePlugin(bot, {}) + +Discord.startBot(client) +``` + +Of course, if you add more and more commands and as your code base grows, you can lose track very quickly. + +To avoid this, it is recommended to store the commands in separate folders divided into different categories. + +[Previously, we introduced you to our plugin structure, which has a lot of advantages.](../design.md) + +```root +├Plugins/ +├── General/ +│ ├── commands/ +│ │ ├── ping.js +│ │ └── ... +├── Developer/ +│ ├── commands/ +│ │ ├── eval.js +│ │ └── ... +└── ... +``` + +**Get [this file](https://github.com/discordeno/discordeno/tree/main/template/nodejs/Managers/CommandManager.js) from +the [nodejs template](https://github.com/discordeno/discordeno/tree/main/template)** + +```js +const CommandManager = require('./template/Managers/CommandManager.js') +const manager = new CommandManager({}) +manager.load({ plugin: true }) // Load the commands +client.commands = manager + +client.commands.cache.get('ping') // Get the `ping` command +``` + +The Manager will automatically iterate through all files in the folder and then load them into the cache property, which +is mapped on the command name. + +**Take a look at [Create Command](./create-command.md) to learn how to create a command.** + +## Handle Command + +The manager also contains a handler for executing the command when a message is received. + +:::important + +Currently checks for permissions, cooldowns, and rate limits are not covered, but these will be added soon. + +::: + +### Message Create Event: + +```js +module.exports = async (client, message) => { + client.commands.isCommand(message) +} +``` + +### Interaction Create Event: + +```js +module.exports = async (client, interaction) => { + client.commands.isInteraction(interaction) +} +``` + +You can also customize the `isCommand` function to your use case. diff --git a/website/old_docs/nodejs/CommandHandler/create-command.md b/website/old_docs/nodejs/CommandHandler/create-command.md new file mode 100644 index 000000000..806d71c61 --- /dev/null +++ b/website/old_docs/nodejs/CommandHandler/create-command.md @@ -0,0 +1,61 @@ +--- +sidebar_position: 3 +--- + +# Create Command + +One of the most important features we wanted in our template, was that you can use the same code for handling +`slash commands` and `message based commands`. + +This can be done by saving the static class in the command cache, creating a constructor and passing the desired data. +Moreover the `BaseCommand` is extended with the `Response Command` class, so you can take advantage of functions such as +`.reply()` + +**Copy the [`BaseCommand`](https://github.com/discordeno/discordeno/tree/main/template/nodejs/Structures/BaseCommand.js) +& +[`CommandResponses`](https://github.com/discordeno/discordeno/tree/main/template/nodejs/Structures/CommandResponses.js) +code from the template** + +### Creating a Ping Command: + +```js +const BaseCommand = require("../../../Structures/BaseCommand.js"); +const Embed = require("../../../Structures/Embed.js"); + +class pingCommand extends BaseCommand { + static name = "ping"; + static description = "See if the bot latency is okay"; + static usage = ""; + static category = "General"; + static slash = { name: "ping", category: "info" }; + + constructor(data) { + super(data); + } + + async execute() { + const msg = await this.reply({content: `Pinging...`}); + // Assign properties to the response + const ping = msg.timestamp - this.message.timestamp; + + const embed = new Embed() + .setTitle(`The Bots ping is ${ping} ms`) + .toJSON(); + + // Edit Message with the Embed + return await msg.edit({embeds: [embed] }); + }); + } +} + +module.exports = pingCommand; +``` + +- The `BaseCommand` is extended with the `CommandResponses` class. +- The ping command class is extended with the `BaseCommand` class. +- Some static properties are assigned to the ping command class, in order to access it in the cache, such as `name`, + `description`, `usage`, `category` and `slash`... +- The `execute()` function will be called, when the command has been run by the user. +- The constructor allows to access data, such as `this.message`, `this.args`, `this.client`... + +You can customize the `CommandManager` file, in order to pass arguments in the `execute()` function. diff --git a/website/old_docs/nodejs/CommandHandler/getting-started.md b/website/old_docs/nodejs/CommandHandler/getting-started.md new file mode 100644 index 000000000..622175240 --- /dev/null +++ b/website/old_docs/nodejs/CommandHandler/getting-started.md @@ -0,0 +1,22 @@ +--- +sidebar_position: 1 +--- + +# Getting Started with the Command Manager + +One of the most important characteristics of bots is that they have commands that can be used to interact with the bot. + +Hard coding your commands in an event function is not the best code practice and should be strictly prevented. + +In the following we will show you how to create a command manager, which is compatible with Discordeno's Client. + +- Load Commands +- Handle Commands +- Reload Commands + +:::info template + +You can also copy the +[`CommandManager` from the template repo](https://github.com/discordeno/discordeno/tree/main/template/nodejs/Managers/CommandManager.js). + +::: diff --git a/website/old_docs/nodejs/EventHandler/_category_.json b/website/old_docs/nodejs/EventHandler/_category_.json new file mode 100644 index 000000000..8dc1b47c3 --- /dev/null +++ b/website/old_docs/nodejs/EventHandler/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Event Handler", + "position": 8 +} diff --git a/website/old_docs/nodejs/EventHandler/event-manager.md b/website/old_docs/nodejs/EventHandler/event-manager.md new file mode 100644 index 000000000..9acf2eb79 --- /dev/null +++ b/website/old_docs/nodejs/EventHandler/event-manager.md @@ -0,0 +1,123 @@ +--- +sidebar_position: 2 +--- + +# Create Event Manager + +In order to process certain events, you must provide the Discordeno client with functions for these events. + +```js +const Discord = require('discordeno') +const config = require('./config.json') + +const client = Discord.createBot({ + events: { + ready(client, payload) { + console.log( + `Successfully connected Shard ${payload.shardId} to the gateway`, + ) + }, + + async messageCreate(client, message) { + if (message.content === '!ping') { + await client.helpers.sendMessage(message.channelId, { content: 'pong' }) + } + + console.log(`Received message: ${message.content || message.embeds}`) + }, + }, + intents: ['Guilds', 'GuildMessages'], + token: config.token, +}) + +Discord.startBot(client) +``` + +As you listen to more and more events, the functions code grows along with them, so you can quickly lose track. + +To avoid this, we recommend storing the event functions divided into files in a separate folder. + +## Create Event Folder + +Create a folder called `events` in your project folder. + +:::info note + +The event files have to be named using camelCase so that they can be understood by the client. e.g `message` -> +`messageCreate.js`. You can check the typings see how the events are called. + +::: + +Ready Event: + +```js +module.exports = (client, payload) => { + if (payload.shardId + 1 === client.gateway.maxShards) { + // All Shards are ready + console.log( + `Successfully connected to the gateway as ${payload.user.username}#${payload.user.discriminator}`, + ) + } +} +``` + +## Load your Events + +```js +const fs = require('fs') +const path = require('path') + +const resolveFolder = folderName => path.resolve(__dirname, '.', folderName) + +class EventManager { + constructor(client) { + this.cache = new Map() + this._events = {} + } + + load(options = {}) { + const eventsFolder = resolveFolder('../events') + fs.readdirSync(eventsFolder).map(async file => { + if (!file.endsWith('.js')) return + + const fileName = path.join(eventsFolder, file) + const event = require(fileName) + const eventName = file.split('.')[0] + + this._events[`${eventName}`] = event + }) + + return this._events + } +} + +module.exports = EventManager +``` + +The code above, which can also be found in the +[template repo](https://github.com/discordeno/discordeno/tree/main/template/nodejs/Managers/EventManager.js) will loop +through all the files in the `events` folder and load the functions into the `_events` object. + +In order to let the client know which events should be processed, you need to pass the functions in the +`createBot.events` object. + +```js +const Discord = require('discordeno') +const config = require('./config.json') + +const EventManager = require('./Managers/EventManager.js') +const events = new EventManager({}) + +const client = Discord.createBot({ + events: events.load({}), + intents: ['Guilds', 'GuildMessages'], + token: config.token, +}) + +Discord.startBot(client) +``` + +Moreover, you can customize the `EventManager` and add more functionality to it and make it exactly fit your needs or +even emit events, by extending it. + +Of course you wonder what you can do with all of this now. We will explain this further on the next page. diff --git a/website/old_docs/nodejs/EventHandler/getting-started.md b/website/old_docs/nodejs/EventHandler/getting-started.md new file mode 100644 index 000000000..350c2e655 --- /dev/null +++ b/website/old_docs/nodejs/EventHandler/getting-started.md @@ -0,0 +1,29 @@ +--- +sidebar_position: 1 +--- + +# Getting Started with the Event Handler + +An event handler is essential to process the data, which Discord sends to you. + +With a good implementation, you will have a nice code structure and thus have a good overview in long term. + +Since the `EventEmitter` class is commonly used you probably already know it from other libraries. + +Discordeno decided against it as it comes with several downsides which are mentioned below. + +- It's easy to create memory leaks, when you add too many listeners or go carelessly with it. +- Many fragmented parts of event code complicate maintenance. +- ErrorHandling is difficult and debugging is harder when many listeners are open for the same events. + +Performance plays a more important role than handling, however this event management system can be easily implemented +since it only needs a few changes in your code. + +In the following we will show you, how to create an event manager, which is compatible with Discordeno's Client. + +:::info template + +You can also copy the +[`EventManager` from the template repo](https://github.com/discordeno/discordeno/tree/main/template/nodejs/Managers/EventManager.js). + +::: diff --git a/website/old_docs/nodejs/EventHandler/handle-event.md b/website/old_docs/nodejs/EventHandler/handle-event.md new file mode 100644 index 000000000..a09cd928d --- /dev/null +++ b/website/old_docs/nodejs/EventHandler/handle-event.md @@ -0,0 +1,76 @@ +--- +sidebar_position: 3 +--- + +# Handle Events + +When an event is fired, Discordeno sends two important things: the `client` instance and the `payload`. + +As mentioned in the `Structure` section, the `payload` object does not contain any functions as it's a plain json +object. + +In order to take use of our nice built structures, we need to transform the payload into a structure. + +:::info + +The Structures can be found [here](https://github.com/discordeno/discordeno/tree/main/template/nodejs/Structures) + +::: + +Sometimes it's important to listen to events, in order to get informed of changes and updating the cache based on it. + +### Message Event + +This file should be called `messageCreate.js`. + +```js +const Message = require('./structures/Message') + +module.exports = async (client, payload) => { + const message = client.messages.forge(payload) + + if (message.author.bot) return + if (message.content === '!ping') return await message.reply('pong') +} +``` + +### Interaction Event + +This file should be called `interactionCreate.js`. + +```js +const Interaction = require('./structures/Interaction') + +module.exports = async (client, payload) => { + const interaction = client.interactions.forge(payload) + + if (interaction.data.name === 'ping') + return await interaction.reply({ content: 'pong' }) +} +``` + +### Ready Event + +This file should be called `ready.js`. + +:::tip + +There is a small difference with the `ready` Event. The Event is fired `shard` wise, in other words it fires every time +a `shard` becomes ready. + +::: + +In order to fire the "real event" a small code snippet has to be added to the `ready` Event. + +```js +const User = require('../Structures/User') + +module.exports = async (client, payload) => { + client.user = client.users.forge(payload.user) + + if (payload.shardId + 1 === client.gateway.maxShards) { + // All Shards are ready + console.log(`Successfully connected to the gateway as ${client.user.tag}`) + } +} +``` diff --git a/website/old_docs/nodejs/Structures/_category_.json b/website/old_docs/nodejs/Structures/_category_.json new file mode 100644 index 000000000..b7a1b76c1 --- /dev/null +++ b/website/old_docs/nodejs/Structures/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Structures", + "position": 7 +} diff --git a/website/old_docs/nodejs/Structures/collectors.md b/website/old_docs/nodejs/Structures/collectors.md new file mode 100644 index 000000000..63aa53d27 --- /dev/null +++ b/website/old_docs/nodejs/Structures/collectors.md @@ -0,0 +1,56 @@ +--- +sidebar_position: 5 +--- + +# Create Collectors + +Some of your commands or features are sometimes based on user interactions. E.g. if a user presses a button and you want +to know whether it was pressed. This is actually done by listening to the `interactionCreate` event. + +But sometimes you need to access locale variables or don't want to "hardcode" the part. + +That's why it's sometimes recommended to create collectors. + +Collectors are listeners that listen to a specific event. In addition, you can provide a filter, so you only receive +certain interactions. + +## Use a Collector + +:::note Template The template code is used below. You must have the EventManager part to use the collector feature. ::: + +We have a pre-made class for collectors which you can find +[here](https://github.com/meister03/discordeno.js/blob/master/Util/Collectors.js). + +```js +const Discord = require('discordeno.js') +const filter = m => + m.data?.customId === 'warn_modal' && m.user.id === interaction.user.id +const listener = client.eventListener // When the eventListener property is named different +const collector = new Discord.Collector('interactionCreate', { + client: client, + timeout: 60000, + filter, + max: 20, + listener, +}) +collector.on('collect', m => { + const interaction = client.interactions.forge(m) + // Stop Collector + // collector.stop(); +}) + +// Fires on a timeout, when the collector has reached the max amount of interactions or when it has been closed +collector.on('end', collected => { + // Map of Collected Interactions + console.log(collected) +}) +``` + +As you can see, this opens up many possibilities. You can listen to any event and get the interaction you need. + +### Collector Options + +`filter`: Function, just fire the event if the filter returns true. `timeout`: Number, the time in milliseconds until +the collector times out. `max`: Number, the max amount of interactions the collector can collect. `listener`: Function, +the listener that will be fired when the collector collects an interaction. Just required when client property is named +differently. diff --git a/website/old_docs/nodejs/Structures/components.md b/website/old_docs/nodejs/Structures/components.md new file mode 100644 index 000000000..6f938cc86 --- /dev/null +++ b/website/old_docs/nodejs/Structures/components.md @@ -0,0 +1,229 @@ +--- +sidebar_position: 4 +--- + +# Create Components + +Since Discord has decided to make message content accessible only to privileged bots, components will play an +increasingly important role in the future. Discord has released some components already and many more will follow. Of +course, this opens up completely new possibilities. On the one hand, it improves the user experience and on the other +hand, the interactions can be easily handled by the developer. + +To take advantage of this, we'll go into more detail on how to use them. + +:::note Runtime Overhead + +Constructor classes are nice to use and make your code look better, but they incur a slight runtime overhead compared to +just using raw data because they still execute methods, which takes more time to process. + +::: + +We already have a Template for `Components`, which can be found +[here](https://github.com/meister03/discordeno.js/tree/master/Structures/Component.js). + +## Different Components: + +There are many different components, which you can quickly read about here: + +### Action Row (`type: 1`): + +This is a top level component, which contains a limited amount of other components. It can be described as container. + +An Action Row ... + +- can not include an action row +- can maximal have 5 Buttons +- can have 1 SelectMenu +- can have 1 Text Input (only available in modal responses) + +### Button (`type: 2`): + +Buttons are interactive components, are bound to a message and they sent an interaction payload, when a user clicks on +it. + +![Different Button Styles](https://i.imgur.com/jUE2Kp0.png) + +- Needs a customId, except the Link Button +- An Action Row can have maximal 5 Buttons + +There are different styles of buttons, which can be used: + +- `1` - PRIMARY - blurple - customId required +- `2` - DEFAULT - grey - customId required +- `3` - SUCCESS - green - customId required +- `4` - DANGER - red - customId required +- `5` - LINK - grey - url required + +### Select Menu (`type: 3`): + +Select Menus are a simple drop-down with selectable options. They accept a set of allowed selects, which sends an +interaction payload, when a user selects sth. from the menu. + +![Select Menu](https://i.imgur.com/42Hwiuw.png) + +- You can specify a range of allowed selects (`minValue` and `maxValue`) +- Every Select Item can have an `emoji` and has a `value`, in order to identify the selected item +- A default Select Item can be set +- An Action Row can have maximal 1 Select Menu + +### Text Input (`type: 4`): + +Text Inputs are interactive components, which can just be sent with a modal response. + +- You can specify a range of text length (`minLength` and `maxLength`) +- You can add a placeholder, a pre-filled value and specify whether the text input is required +- An Action Row can have maximal 1 Text Input + +## Send Components + +As mentioned above there are different types of components. This requires to define a type, so that Discord knows, which +component you want to use. + +```js +class ActionRow { + constructor(options = {}) { + this.type = 1 + } + + setComponents(...components) { + this.components = components + return this + } +} +``` + +```js +const button = new Button() +const button2 = new Button() +const actionRow = new ActionRow().setComponents(button, button2) +``` + +This code will obviously not work because it's a missing a lot required of data. The other reason is that we can't send +a class to Discord, we need sth. to transform it to a json object. + +We have a pre-made class for components which you can find +[here](https://github.com/meister03/discordeno.js/tree/master/Structures/Component.js). + +### Button + +```js +const Discord = require('discordeno.js') +const message = client.messages.forge(rawMessage) + +const button = new Discord.Component() + .setType('BUTTON') + .setStyle('LINK') + .setLabel('Click me!') + .setUrl('https://google.com') + .toJSON() + +// Button with raw types +const button2 = new Discord.Component() + .setType(2) + .setStyle(4) + .setLabel('DO NOT CLICK') + .setCustomId('12345') + .toJSON() + +const actionRow = new Discord.Component() + .setType('ACTION_ROW') + .setComponents(button, button2) + .toJSON() + +// Message to send +const messageOptions = { content: 'hello', components: [actionRow] } + +// await client.helpers.sendMessage(channelId, messageOptions); // Do it the raw way +message.channel.send(messageOptions) // Do it with the structure +``` + +As you can see, for simplicity you can use strings instead of numbers (types), which are hard to remember. + +### Select Menu + +```js +const Discord = require('discordeno.js') +const message = client.messages.forge(rawMessage) + +const selectMenu = new Discord.Component() + .setType('SELECT_MENU') + .setCustomId('12345') + .setOptions([ + { + label: 'Option 1', + value: '1', + description: `This is option 1`, + }, + { + label: 'Option 2', + value: '2', + description: `This is option 2`, + }, + { + label: 'Default Option', + value: '3', + description: `Default option...`, + default: true, + }, + ]) + .setPlaceholder('Select an option') + .toJSON() + +const actionRow = new Discord.Component() + .setType('ACTION_ROW') + .setComponents(selectMenu) + .toJSON() + +const messageOptions = { content: 'hello', components: [actionRow] } + +// await client.helpers.sendMessage(channelId, messageOptions); // Do it the raw way +message.channel.send(messageOptions) // Do it with the structure +``` + +### Text Input + +```js +const Discord = require('discordeno.js') +const interaction = client.messages.forge(rawInteraction) + +const textInput = new Component() + .setType('TEXT_INPUT') + .setStyle('SHORT') + .setCustomId('t1') + .setLabel('User ID') + .setPlaceholder('User ID') + .setRequired(true) + .setMaxLength(20) + .setMinLength(1) + .toJSON() + +const textInput2 = new Component() + .setType('TEXT_INPUT') + .setStyle('PARAGRAPH') + .setCustomId('t2') + .setLabel('Reason') + .setPlaceholder('Reason for Ban') + .setRequired(false) + .setMaxLength(300) + .toJSON() + +const actionRow = new Component() + .setType('ACTION_ROW') + .setComponents(textInput) + .toJSON() +const actionRow2 = new Component() + .setType('ACTION_ROW') + .setComponents(textInput2) + .toJSON() + +interaction.popupModal({ + customId: 'ban_modal', + title: 'Ban User', + components: [actionRow, actionRow2], +}) +``` + +### Receive Interactions + +When a user clicks a button or selects an option from a Select Menu, Discord sends an `interactionCreate` event, which +contains the information necessary to process it. diff --git a/website/old_docs/nodejs/Structures/create-structure.md b/website/old_docs/nodejs/Structures/create-structure.md new file mode 100644 index 000000000..7bbaa6c4d --- /dev/null +++ b/website/old_docs/nodejs/Structures/create-structure.md @@ -0,0 +1,100 @@ +--- +sidebar_position: 2 +--- + +# Create Structure + +Structures are often used to transform data and add methods to existing objects. To make it easier to work with them. + +Imagine you have a channel object to which you want to send a message. + +```js +const data = { + id: 806947972004839444n, + name: 'spam-and-bots', +} +``` + +The recommended way would be: + +```js +await client.helpers.sendMessage(data.id, { content: 'hello' }) +``` + +However, you probably want to use something shorter, such as the following: + +```js +class Channel { + constructor(client, data) { + this.client = client + this.id = data.id + this.name = data.name + } + + async send(options) { + return await this.client.helpers.sendMessage(this.id, options) + } +} +``` + +Now you can use the `.send()` method on the channel object without using such a long code: + +```js +const channel = new Channel(client, data) +await channel.send({ content: 'hello' }) +``` + +Moreover, you can modify the `.send()` method to better suit your use case e.g not send the message if the channel is +blacklisted. + +This naturally opens a lot of opportunities and makes coding a lot easier. Because you decide what you want to do with +the data, how the methods are named and how you want to process the request. + +## Using Template Structures: + +When you are migrating from another library and you want to utilize the djs-like wrapper, you'll likely choose to +continue using special structures. Therefore we have ready-made structures for the wrapper `Discordeno.js`. + +- [Guild](https://github.com/meister03/discordeno.js/tree/master/Structures/Guild.js) +- [Channel](https://github.com/meister03/discordeno.js/tree/master/Structures/Channel.js) +- [Role](https://github.com/meister03/discordeno.js/tree/master/Structures/Role.js) +- [Member](https://github.com/meister03/discordeno.js/tree/master/Structures/Member.js) +- [User](https://github.com/meister03/discordeno.js/tree/master/Structures/User.js) +- [Message](https://github.com/meister03/discordeno.js/tree/master/Structures/Message.js) +- [Interaction](https://github.com/meister03/discordeno.js/tree/master/Structures/Interaction.js) +- [Emoji](https://github.com/meister03/discordeno.js/tree/master/Structures/Emoji.js) +- [Webhook](https://github.com/meister03/discordeno.js/tree/master/Structures/Webhook.js) +- [Embed](https://github.com/meister03/discordeno.js/tree/master/Structures/Embed.js) +- [Component](https://github.com/meister03/discordeno.js/tree/master/Structures/Component.js) +- [Collection](https://github.com/meister03/discordeno.js/tree/master/Structures/Collection.js) + +We recommend that you check the wrappers [Readme](https://github.com/meister03/discordeno.js#discordclient) in order to +construct the client for following the Guide + +**Using the Structures:** + +```js +const Discord = require('discordeno.js') +const client = new Discord.Client(clientOptions, cacheOptions) //See the Readme above +Discord.startBot(client) +const guild = client.guilds.forge(guildData) +const channel = guild.channels.forge(channelData) +const role = guild.roles.forge(roleData) +const member = guild.members.forge(memberData) +const user = guild.users.forge(userData) +const message = guild.messages.forge(messageData) +const interaction = guild.interactions.forge(interactionData) +const emoji = guild.emojis.forge(emojiData) + +const webhook = new Discord.Webhook(client, webhookData) +const embed = new Discord.Embed(embedData) // embedData is optional +const component = new Discord.Component(componentData) // componentData is optional +const collection = new Discord.Collection() +``` + +Some popular methods have been added to the structures so that you can use them without having to come up with your own. +In order to use the Structures from the Wrapper, you need to invoke the `.forge` method with the raw discord data, +whereas it will construct the structure for you. + +Next we're going to give a better insight into how create [`Embeds`](embeds) and [`Components`](components) with the +wrappers structures. diff --git a/website/old_docs/nodejs/Structures/embeds.md b/website/old_docs/nodejs/Structures/embeds.md new file mode 100644 index 000000000..fa40b8b39 --- /dev/null +++ b/website/old_docs/nodejs/Structures/embeds.md @@ -0,0 +1,112 @@ +--- +sidebar_position: 3 +--- + +# Create Embeds + +Embeds are widely used by bots in order to display messages in a fancy way. + +Unfortunately, the Discord API does not accept funky classes such as `new MessageEmbed().setTitle("hello")`, instead it +takes a json object, e.g. `{ title: "hello" }`. Therefore, we need to create an embed Structure that converts the +user-supplied data into the format which Discord uses. + +:::note Runtime Overhead + +Constructor classes are nice to use and make your code look better, but they incur a slight runtime overhead compared to +just using raw data because they still execute methods, which takes more time to process. + +::: + +```js +class Embed() { + constructor() {} + + setTitle(title) { + this.title = title; + } +} +``` + +Now we have created a class which we can use to create embeds. But we can't just send this to Discord. + +So we need an additional method which will convert the data from the class to the correct format. + +```js +class Embed(){ + constructor() {} + + setTitle(title) { + this.title = title; + } + + toJSON() { + return { + title: this.title + } + } +} +``` + +Wow, now you can create a embed and send it to Discord. + +```js +const Channel = require('./structures/Channel') // Path to structure + +const channel = new Channel(client, data) +await channel.send({ embeds: [embed] }) +``` + +You probably want more methods which you can use to create embeds. +[Here is how the Embed Structure looks like](https://github.com/meister03/discordeno.js/blob/master/Structures/Embed.js) + +### Using the Embed Structure: + +```js +const Discord = require('discordeno.js') + +const channel = client.channels.forge(channelData) +const showCaseEmbed = new Discord.Embed() + .setColor(0x00ae86) + .setTitle('A Random Title') + .setURL('https://github.com/discordeno') + .setAuthor({ + name: 'Author name', + iconUrl: + 'https://raw.githubusercontent.com/discordeno/discordeno/main/site/static/img/logo.png', + url: 'https://github.com/discordeno', + }) + .setDescription('A Random Description') + .setThumbnail( + 'https://raw.githubusercontent.com/discordeno/discordeno/main/site/static/img/logo.png', + ) + .addFields( + { name: 'Field 1 Name', value: 'Normal Field Value' }, + { name: '\u200B', value: '\u200B' }, + { name: 'Field 2 Name', value: 'Inline Field Value', inline: true }, + { name: 'Field 3 Name', value: 'Inline Field Value', inline: true }, + ) + .addField({ name: 'Field 4', value: 'Field Value' }) + .setImage( + 'https://raw.githubusercontent.com/discordeno/discordeno/main/site/static/img/logo.png', + ) + .setTimestamp() + .setFooter({ + text: 'A Footer Text', + iconUrl: + 'https://raw.githubusercontent.com/discordeno/discordeno/main/site/static/img/logo.png', + }) + .toJSON() + +await channel.send({ embeds: [showCaseEmbed] }) +``` + +### Embed Limits: + +- Title: 256 characters +- Description: 4096 characters +- Field Name: 256 characters +- Field Value: 1024 characters +- Footer Text: 2048 characters +- Author Name: 256 characters +- 10 Embeds per message +- In total over all 10 Embeds not more than 6000 characters diff --git a/website/old_docs/nodejs/Structures/getting-started.md b/website/old_docs/nodejs/Structures/getting-started.md new file mode 100644 index 000000000..b06806d6d --- /dev/null +++ b/website/old_docs/nodejs/Structures/getting-started.md @@ -0,0 +1,30 @@ +--- +sidebar_position: 1 +--- + +# Getting Started with Structures + +As previously mentioned, Discordeno was built with as few classes as possible, this is in favor of performance. + +For example, you cannot execute functions on objects. + +```diff +- message.channel.send({content: "hello"}) ++ client.helpers.sendMessage(message.channel.id, {content: "hello"}) +``` + +This seems to be more complicated at first, but has many advantages: + +- You get full control over the actions +- Errors are easier to debug +- A validation by classes does not have to take place + +One of the disadvantages is that you have to change a lot in your code. + +Of course, we recommend that you try out the upper way, but we will introduce structures in this guide because they are +used by many users who eventually want to migrate. + +For example, if you want to get correctly formatted objects, structures are obviously beneficial, because they support +the readability of the code by their ease of use + +In the following, we will introduce how to create your own structures and how to use the ones available in the template. diff --git a/website/old_docs/nodejs/_category_.json b/website/old_docs/nodejs/_category_.json new file mode 100644 index 000000000..b45c7e9ac --- /dev/null +++ b/website/old_docs/nodejs/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Nodejs", + "position": 3 +} diff --git a/website/old_docs/nodejs/create-application.md b/website/old_docs/nodejs/create-application.md new file mode 100644 index 000000000..b7197a8ba --- /dev/null +++ b/website/old_docs/nodejs/create-application.md @@ -0,0 +1,29 @@ +--- +sidebar_position: 3 +--- + +# Create Application + +1. Go to the [Developer Portal](https://discord.com/developers/applications) and create a new application. +2. Navigate to the Section `Bot` and confirm with "Yes, do it!" +3. Now copy your token and save it under a safe environment. + +:::caution Token Security + +Keep your token safe, because it is like a password that grants access to your bot, which then can be used for mass +DMing, mass banning or any other kind of malicious activity. + +::: + +## Add your Bot to your Server + +In order to use your Bot, it should be in a server where you can interact with it. + +1. Go to the [Developer Portal](https://discord.com/developers/applications) and click on your previously created bot. +2. Click on `OAuth2` and there go to the `URL Generator`. +3. Select the `bot` and the `applications.commands` scope. +4. Scroll down and select the `Administrator` permission. +5. Copy the generated URL and open it in your browser. +6. Select your Server and click the invite button. + +The bot should now have been added to your server and show as an offline user. diff --git a/website/old_docs/nodejs/design.md b/website/old_docs/nodejs/design.md new file mode 100644 index 000000000..86dacc19b --- /dev/null +++ b/website/old_docs/nodejs/design.md @@ -0,0 +1,203 @@ +--- +sidebar_position: 6 +--- + +# Design + +In order to ensure long-term scalability and maintainability, the code structure is of enormous importance. In the +following, we show how such a code structure could look like. + +The essential parts are a `CommandHandler/CommandManager`, `EventHandler/EventManager`, lots of `Structures` in order to +code faster and `Plugins`, where your different features will be, such as `Commands`, `DB Stuff`... + +## Code Structure + +We recommend following structure for your code: + +```root +├index.js +├─Structures/ +├─Managers/ +├─events/ +├─Plugins/ +├── General/ +│ ├── commands/ +│ │ ├── ping.js +│ │ └── ... +├── Developer/ +│ ├── commands/ +│ │ ├── eval.js +│ │ └── ... +├─Util/ +└── ... +``` + +The following explains why this structure is suitable. If you want to follow this guide further, you should create these +folders. + +In the `Managers` folder the Managers will be added e.g. `CommandManager.js`, `EventManager.js`. Generally codes, which +manage the system. + +While in the `Structures` folder mainly classes are added like `BaseCommand.js`, `CommandResponse.js`, `Embed.js`, +`Components.js`, which make it easier to add methods to objects. + +The `events` folder will contain the event handlers such as `messageCreate.js`, `debug.js` + +Your many useful features and categories end up in the `Plugins` folder, where they should be categorically divided into +many folders. + +The `Util` folder contains functions or classes that help you convert certain things, such as timestamps, into a +human-readable format. + +## CommandHandler & BaseCommand + +The `CommandHandler` is the main class of the bot, which will handle all the commands and the events received from +Discord. + +The `BaseCommand` is the base class of all commands, which will be extended with the`CommandResponse` class. + +### Steps showed in the following Guide + +- Loading commands from different plugins +- Deploying slash commands +- Handling `messageCreate` & `interactionCreate` events +- Command rate limit handling +- Handle `Interaction` & `Message` commands with the same code +- Validating user provided arguments +- Correct permission and error handling +- Hot reloading commands +- Creating message and interaction collectors + +## EventHandler + +You probably realized that Discordeno does not use an `EventEmitter` to fire the events, but your own event function is +fired. + +There are ways to adapt to an `EventEmitter`, but we decided against it for the following reasons: + +- It's easy to create memory leaks, when you add too many listeners or go carelessly with it. +- Many fragmented parts of event code complicate maintenance. +- ErrorHandling is difficult and debugging is harder when many listeners are open for the same events. + +## Structures + +Structures are essential to abstract larger parts of code in smaller ready-made methods and to modify them if necessary. + +Example: + +```js +class Command { + static name = 'ping' + static aliases = ['pong'] + static botPermission = ['SEND_EMBED_LINKS'] + + run(message, args) { + // do something + } +} +``` + +It would be annoying adding everytime the `botPermission` property to the class Command, when the Permission is used +from every Command, then it is unnecessary to add it, when you can extend the class. + +It would be annoying to add the `botPermission` property to the command class every time the same permissions are used +by each command. Extending the class makes this extra step obsolete. + +```js +class BaseCommand { + constructor(client) { + this.client = client + this.basePermission = ['SEND_EMBED_LINKS'] + } +} + +class Command extends BaseCommand { + static name = 'ping' + static aliases = ['pong'] + + constructor(data) { + super(data) + } + + run(message, args) { + // do something + } +} +``` + +## Plugins + +The plugins folder helps you categorize your code into many parts to give some structure. + +Of course, this has many advantages, you have a much clearer code, you can debug problems much easier. + +This also opens possibilities for open source contributions, since not all parts of the code have to be published in +order to add new plugins, since they are "independent". + +There will be the main `Plugins` folder, which by default contains a `General` folder for all your base commands. The +`Plugins` folder will also contain all your other plugins. + +## Error Handling + +One of the most important things is how to handle errors. This is done to provide a user-friendly experience and to find +errors faster. + +You should catch errors and log them in your logger so you can fix them later. There are several open source `Sentry`'s +that give you a good overview of the latest errors through a website. + +Sometimes errors have a positive effect on maintainability and scalability. + +In addition, handling errors caused by users is very important to increase transparency. If they don't know why the +error happened, then they'll be very surprised with what they did wrong and might even remove your bot from their +server. + +## Caching + +Normally libraries cache all the info they get, which can of course be helpful at the beginning to discover all +functionalities but later it turns out to be a resource-consuming method. Therefore, this way should be avoided. + +Discordeno allows `Custom Caching` and even `Custom Property Caching` which gives you fine-grained control over the +caching of data. Normally you only need 20% of the data received by Discord, which makes caching unnecessary in most +cases. + +There are also some `Filter` and `Sweeper` methods which help you to empty unused cache values. + +## Cross Communication & Scaling + +If you are running many different processes, such as a Welcomer API, communication is of central importance in order to +send or receive data, with which you can then perform certain actions. + +Cross communication can be easily done with sockets or a TCP client. + +This brings up this Structure: + +```js +Bridge (Heart) +- Machine 1 + - Cluster [0-9] +- Machine 2 + - Cluster [10-18] +- Machine 3 -> Welcomer Api +- Machine 4 -> Dashboard +``` + +It's important to use something fast to have a proper "real time" communication. + +Discordeno already offers many internal options for scaling bots, no matter what size. + +As you scale, you will likely separate many parts of your bot and put them in separate processes, such as a +`RestManager`, a `Gateway Manager` etc. + +This of course opens up a lot of possibilities: + +- Zero downtime updates +- Global cache +- Synced rate limits + +[Check the Github Readme for more information](https://github.com/discordeno/discordeno#features) + +:::tip congratulations + +You just learned how to design a scalable bot, let's get into implementing it with the next pages. + +::: diff --git a/website/old_docs/nodejs/getting-started.md b/website/old_docs/nodejs/getting-started.md new file mode 100644 index 000000000..04a65d5d3 --- /dev/null +++ b/website/old_docs/nodejs/getting-started.md @@ -0,0 +1,48 @@ +--- +sidebar_position: 1 +--- + +# Getting Started + +If you are reading this, you probably want to create a Discord bot with Discordeno or migrate from popular libraries like Discord.js. + +If this is going to be your first time making a bot, you should use Deno instead of Node.js. Although in some cases, Deno might not be suitable for you because of missing packages or a code base that is too large to migrate to a slightly different language. + +This guide will help you make your first Discord bot using Node.js or even migrate your bot from another library. + +Moreover this guide will utilize two different options. One option to use the Discordeno package without any frameworks +and one, which uses the wrapper called `Discordeno.js`, which aims to achieve a djs-like interface. + +:::important Disclaimer + +Some features are not documented yet. If you want to know more about them, kindly ask for help in the +[Discord Server](https://discord.gg/ddeno). + +::: + +## Why should I switch? + +Discordeno was built with the purpose of being scalable, flexible and easy to use. + +Libraries like `Discord.js` and `Eris` often have excessive caching behavior that can only be changed slightly without +breaking the entire library. There is a lack of customization and many nested classes, which makes it almost impossible +to edit the code without having unwanted side effects. Moreover scalability is only possible on a limited extend. + +Discordeno has been kept plain and simple, which opens up a lot of opportunities for customization such as +`custom-caching (custom-property-caching)`, [`Standalone Rest`](../big-bot-guide/rest.md), +[`Gateway`](../big-bot-guide/gateway.md), [`Cache`](../big-bot-guide/cache.md) and more. Check the detailed advantages +[here](https://github.com/discordeno/discordeno#features). + +This guide will also help you making your code more scalable and easier to maintain with bringing you closer to the +Discord API. + +# Before you start + +Before you start digging in this guide, you should have a solid understanding of `javascript`. If you are not familiar +with it, then you should take a look at some popular resources. + +- [W3Schools Course](https://www.w3schools.com/js/DEFAULT.asp) +- [Mozilla Docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript) +- [JavaScript.Info](https://javascript.info) + +A basic understanding is of great importance in order to solve problems skillfully. diff --git a/website/old_docs/nodejs/initial-setup.md b/website/old_docs/nodejs/initial-setup.md new file mode 100644 index 000000000..76449266f --- /dev/null +++ b/website/old_docs/nodejs/initial-setup.md @@ -0,0 +1,48 @@ +--- +sidebar_position: 4 +--- + +# Initial Setup + +## Config File + +Ideally, you should save your configs in an `.env` file. Out of simplicity for this guide, we are saving it in a +`config.json` file. + +Create a file named `config.json` in your project folder and insert the following content: + +```json +{ + "token": "YOUR_TOKEN_HERE", + "prefix": "!" +} +``` + +## Edit the main file + +Open the `index.js` file which you have created earlier and then insert the following content: + +```js +const Discord = require('discordeno') +const config = require('./config.json') + +const client = Discord.createBot({ + events: { + ready(client, payload) { + console.log( + `Successfully connected Shard ${payload.shardId} to the gateway`, + ) + }, + }, + intents: ['Guilds', 'GuildMessages'], + token: config.token, +}) + +Discord.startBot(client) +``` + +Now you can start your bot by running the following command in your terminal: + +```cli +$ node index.js +``` diff --git a/website/old_docs/nodejs/installion.md b/website/old_docs/nodejs/installion.md new file mode 100644 index 000000000..210daf415 --- /dev/null +++ b/website/old_docs/nodejs/installion.md @@ -0,0 +1,30 @@ +--- +sidebar_position: 2 +--- + +# Installing Node.js and Discordeno + +To use the Discordeno library you first need to install Node.js and then Discordeno from NPM. + +Go to [nodejs.org](https://nodejs.org/en/) and download the latest version of Node.js. Open the downloaded file and +follow the instructions of the installer to install Node.js. + +## Create a Folder + +Open your file manager and create a new folder (e.g.: `discordbot`) in your desired directory. Then open the code editor +of your choice and create a new file (e.g.: `index.js`) in the folder you just have created. + +### Initalize NPM & Install Discordeno + +In order to keep track of the dependencies, you need to initialize NPM, which generates a `package.json` file. + +```cli +$ npm init --yes +``` + +Then you need to install Discordeno. When you want to go along with the wrapper named `Discordeno.js`, then install it +too. Go to your terminal and run the following command: + +```cli +$ npm install discordeno +``` diff --git a/website/old_docs/nodejs/slash-command.md b/website/old_docs/nodejs/slash-command.md new file mode 100644 index 000000000..989bf76fb --- /dev/null +++ b/website/old_docs/nodejs/slash-command.md @@ -0,0 +1,70 @@ +--- +sidebar_position: 5 +--- + +# Slash Commands + +Since Discord has decided to make message content accessible only to privileged bots, message commands will play a +subordinate role in the future. Discord users will be more used to slash commands. That's why it's essential that every +bot offers them. + +In the following we will show you how to create slash commands: + +## Deploying Slash Commands + +There is a difference between global and guild commands. Global commands take a while to appear in all guilds. Guild +commands show up directly. + +For this reason, we will now show how to create guild commands, in order to test them immediately. + +```js +const guildId = BigInt('YOUR_GUILD_ID') +const command = { + name: 'ping', + description: 'Retrieves the Bot latency', + options: [], +} + +client.helpers.createApplicationCommand(command, guildId) +``` + +This is just very simple example, you can also add sub commands, select options and much more. + +## Handling Slash Commands + +Discord sends a WebSocket Event when a user runs a slash command. You can listen to this event by adding the +`interactionCreate` function in the client. + +```js +const Discord = require('discordeno') +const config = require('./config.json') + +const client = Discord.createBot({ + events: { + ready(client, payload) { + console.log( + `Successfully connected Shard ${payload.shardId} to the gateway`, + ) + }, + async interactionCreate(client, interaction) { + if (interaction.data?.name === 'ping') { + return await client.helpers.sendInteractionResponse( + interaction.id, + interaction.token, + { + type: Discord.InteractionResponseTypes.ChannelMessageWithSource, + data: { content: '🏓 Pong!' }, + }, + ) + } + }, + }, + intents: Discord.Intents.Guilds | Discord.Intents.GuildMessages, + token: config.token, +}) + +Discord.startBot(client) +``` + +The handling may see complicated in the beginning, but as mentioned before, we will introduce structures to make it +easier. diff --git a/website/package.json b/website/package.json index 27719e114..4597d7092 100644 --- a/website/package.json +++ b/website/package.json @@ -30,6 +30,7 @@ "react": "^18.2.0", "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", + "reactflow": "^11.10.1", "styled-components": "^6.1.1" }, "devDependencies": { diff --git a/website/sidebars.js b/website/sidebars.js index f3303339e..a78d57e0b 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -15,7 +15,8 @@ const sidebars = { // By default, Docusaurus generates a sidebar from the docs folder structure tutorialSidebar: [{ type: 'autogenerated', dirName: '.' }], - + tutorial: [{ type: 'autogenerated', dirName: '.' }], + old_docs: [{ type: 'autogenerated', dirName: '.' }], // But you can create a sidebar manually /* tutorialSidebar: [ diff --git a/website/src/components/architecture/BaseFlowChart.tsx b/website/src/components/architecture/BaseFlowChart.tsx new file mode 100644 index 000000000..bcde808db --- /dev/null +++ b/website/src/components/architecture/BaseFlowChart.tsx @@ -0,0 +1,125 @@ +import React, { useEffect, useState } from 'react' +import ReactFlow, { + Background, + Controls, + Edge, + Handle, + Node, + Position, + useEdgesState, + useNodesState, +} from 'reactflow' +import 'reactflow/dist/style.css' + +export const multiplier = 225 +export const height = 40 +export const widthMultiplier = 0.75 + +export const defaultNodeOptions = { + targetPosition: Position.Left, + sourcePosition: Position.Right, + draggable: false, + style: { width: `${multiplier * 0.75}px`, height: `${height}px` }, +} + +export const defaultGroupOptions = { + draggable: false, +} + +export default function BaseFlowChart({ + initialNodes = [], + initialEdges = [], +}: { + initialNodes: Node[] + initialEdges: Edge[] +}) { + function getWindowDimensions() { + const { innerWidth: width, innerHeight: height } = window + return { + width, + height, + } + } + + const [windowDimensions, setWindowDimensions] = useState( + getWindowDimensions(), + ) + + useEffect(() => { + function handleResize() { + setWindowDimensions(getWindowDimensions()) + } + + window.addEventListener('resize', handleResize) + return () => window.removeEventListener('resize', handleResize) + }, []) + + const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes) + const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges) + + return ( + <> +
= 997 + ? `${ + (100 * + ((windowDimensions.width - + 300 - + (windowDimensions.width >= 1620 + ? (windowDimensions.width - 1620) * 0.5 + : 0)) / + windowDimensions.width) - + 2) * + widthMultiplier + }vw` + : '95vw', + height: '25vh', + }} + > + ( +
+ + +
+ ), + baseLineNodeText: n => ( +
+

{n.data.label}

+
+ ), + }} + fitView + > + + +
+
+ + ) +} diff --git a/website/src/components/architecture/FlowChart.tsx b/website/src/components/architecture/FlowChart.tsx new file mode 100644 index 000000000..432b9522a --- /dev/null +++ b/website/src/components/architecture/FlowChart.tsx @@ -0,0 +1,165 @@ +import React from 'react' +import { Edge, Node } from 'reactflow' +import 'reactflow/dist/style.css' +import BaseFlowChart, { defaultNodeOptions, multiplier } from './BaseFlowChart' + +const initialNodes: Node[] = [ + { + id: 'discordGateway', + type: 'input', + position: { x: 0 * multiplier, y: 0 }, + data: { label: 'Discord Gateway' }, + ...defaultNodeOptions, + }, + { + id: 'gateway', + position: { x: 1 * multiplier, y: 0 }, + data: { label: 'Gateway' }, + ...defaultNodeOptions, + }, + { + id: 'baseLineNode-1', + type: 'baseLineNode', + position: { x: 0.85 * multiplier, y: 170 }, + data: {}, + }, + { + id: 'baseLineNode-2', + type: 'baseLineNode', + position: { x: 0.85 * multiplier, y: -130 }, + data: {}, + }, + { + id: 'baseLineNode-3', + type: 'baseLineNode', + position: { x: 2.85 * multiplier, y: 170 }, + data: {}, + }, + { + id: 'baseLineNode-4', + type: 'baseLineNode', + position: { x: 2.85 * multiplier, y: -130 }, + data: {}, + }, + { + id: 'baseLineNode-5', + type: 'baseLineNode', + position: { x: 3.85 * multiplier, y: 170 }, + data: {}, + }, + { + id: 'baseLineNode-6', + type: 'baseLineNode', + position: { x: 3.85 * multiplier, y: -130 }, + data: {}, + }, + { + id: 'baseLineNode-7', + type: 'baseLineNode', + position: { x: 4.85 * multiplier, y: 170 }, + data: {}, + }, + { + id: 'baseLineNode-8', + type: 'baseLineNode', + position: { x: 4.85 * multiplier, y: -130 }, + data: {}, + }, + { + id: 'baseLineNodeText-1', + type: 'baseLineNodeText', + position: { x: 0 * multiplier, y: -130 }, + data: { label: 'Discord' }, + }, + { + id: 'baseLineNodeText-2', + type: 'baseLineNodeText', + position: { x: 1.5 * multiplier, y: -130 }, + data: { label: 'Event In' }, + }, + { + id: 'baseLineNodeText-3', + type: 'baseLineNodeText', + position: { x: 3 * multiplier, y: -130 }, + data: { label: 'Event Processing' }, + }, + { + id: 'baseLineNodeText-4', + type: 'baseLineNodeText', + position: { x: 4 * multiplier, y: -130 }, + data: { label: 'Event out' }, + }, + { + id: 'baseLineNodeText-6', + type: 'baseLineNodeText', + position: { x: 5 * multiplier, y: -130 }, + data: { label: 'Discord' }, + }, + { + id: 'bot', + position: { x: 2 * multiplier, y: 0 }, + data: { label: 'Bot' }, + ...defaultNodeOptions, + }, + { + id: 'yourCode', + position: { x: 3 * multiplier, y: 0 }, + data: { label: 'Your Code' }, + ...defaultNodeOptions, + }, + { + id: 'rest', + position: { x: 4 * multiplier, y: 0 }, + data: { label: 'Rest' }, + ...defaultNodeOptions, + }, + { + id: 'discordApiGateway', + type: 'output', + position: { x: 5 * multiplier, y: 0 }, + data: { label: 'Discord Api' }, + ...defaultNodeOptions, + }, +] + +const initialEdges: Edge[] = [ + { id: 'd-g', source: 'discordGateway', target: 'gateway' }, + { id: 'g-b', source: 'gateway', target: 'bot' }, + { id: 'b-y', source: 'bot', target: 'yourCode' }, + { id: 'y-r', source: 'yourCode', target: 'rest' }, + { id: 'r-d', source: 'rest', target: 'discordApiGateway' }, + { + id: 'baseLine-1', + source: 'baseLineNode-1', + target: 'baseLineNode-2', + style: { stroke: 'blue', strokeDasharray: 20 }, + animated: false, + }, + { + id: 'baseLine-2', + source: 'baseLineNode-3', + target: 'baseLineNode-4', + style: { stroke: 'blue', strokeDasharray: 20 }, + animated: false, + }, + { + id: 'baseLine-3', + source: 'baseLineNode-5', + target: 'baseLineNode-6', + style: { stroke: 'blue', strokeDasharray: 20 }, + animated: false, + }, + { + id: 'baseLine-4', + source: 'baseLineNode-7', + target: 'baseLineNode-8', + style: { stroke: 'blue', strokeDasharray: 20 }, + animated: false, + }, +] + +export default function FlowChart() { + return ( + + ) +} diff --git a/website/src/components/architecture/FlowChart2.tsx b/website/src/components/architecture/FlowChart2.tsx new file mode 100644 index 000000000..9fa433b3d --- /dev/null +++ b/website/src/components/architecture/FlowChart2.tsx @@ -0,0 +1,310 @@ +import React from 'react' +import { Edge, Node, Position } from 'reactflow' +import 'reactflow/dist/style.css' +import BaseFlowChart, { + defaultGroupOptions, + defaultNodeOptions, + multiplier, +} from './BaseFlowChart' + +const initialNodes: Node[] = [ + { + id: 'discordGateway', + type: 'input', + position: { x: 0 * multiplier, y: 0 }, + data: { label: 'Discord Gateway' }, + ...defaultNodeOptions, + }, + { + id: 'baseLineNode-1', + type: 'baseLineNode', + position: { x: 0.85 * multiplier, y: 170 }, + data: {}, + }, + { + id: 'baseLineNode-2', + type: 'baseLineNode', + position: { x: 0.85 * multiplier, y: -130 }, + data: {}, + }, + { + id: 'baseLineNode-3', + type: 'baseLineNode', + position: { x: 3.75 * multiplier, y: 170 }, + data: {}, + }, + { + id: 'baseLineNode-4', + type: 'baseLineNode', + position: { x: 3.75 * multiplier, y: -130 }, + data: {}, + }, + { + id: 'baseLineNodeText-1', + type: 'baseLineNodeText', + position: { x: 0 * multiplier, y: -130 }, + data: { label: 'Discord' }, + }, + { + id: 'baseLineNodeText-2', + type: 'baseLineNodeText', + position: { x: 2 * multiplier, y: -130 }, + data: { label: 'Gateway' }, + }, + { + id: 'baseLineNodeText-3', + type: 'baseLineNodeText', + position: { x: 4 * multiplier, y: -130 }, + data: { label: 'Bot' }, + }, + { + id: 'gatewayManager', + type: 'input', + position: { x: 2.0625 * multiplier, y: -80 }, + data: { label: 'Gateway Manager' }, + ...defaultNodeOptions, + targetPosition: Position.Top, + sourcePosition: Position.Bottom, + }, + { + id: 'shard-1', + type: 'output', + position: { x: 1.25 * multiplier, y: -20 }, + data: { label: 'Shard-1' }, + style: { width: `${multiplier * 2}px`, height: '80px' }, + ...defaultGroupOptions, + }, + { + id: 'shard-1-socket', + position: { x: 0.125 * multiplier, y: 20 }, + data: { label: 'webSocket' }, + ...defaultNodeOptions, + parentNode: 'shard-1', + extent: 'parent', + }, + { + id: 'shard-1-handleMessage', + position: { x: 1.125 * multiplier, y: 20 }, + data: { label: 'HandleMessage' }, + ...defaultNodeOptions, + parentNode: 'shard-1', + extent: 'parent', + }, + { + id: 'shard-2', + type: 'output', + position: { x: 1.375 * multiplier, y: 0 }, + data: { label: 'Shard-2' }, + style: { width: `${multiplier * 2}px`, height: '80px' }, + ...defaultGroupOptions, + }, + { + id: 'shard-2-socket', + position: { x: 0.125 * multiplier, y: 20 }, + data: { label: 'webSocket' }, + ...defaultNodeOptions, + parentNode: 'shard-2', + extent: 'parent', + }, + { + id: 'shard-2-handleMessage', + position: { x: 1.125 * multiplier, y: 20 }, + data: { label: 'HandleMessage' }, + ...defaultNodeOptions, + parentNode: 'shard-2', + extent: 'parent', + }, + { + id: 'shard-3', + type: 'output', + position: { x: 1.5 * multiplier, y: 20 }, + data: { label: 'Shard-3' }, + style: { width: `${multiplier * 2}px`, height: '80px' }, + ...defaultGroupOptions, + }, + { + id: 'shard-3-socket', + position: { x: 0.125 * multiplier, y: 20 }, + data: { label: 'webSocket' }, + ...defaultNodeOptions, + parentNode: 'shard-3', + extent: 'parent', + }, + { + id: 'shard-3-handleMessage', + position: { x: 1.125 * multiplier, y: 20 }, + data: { label: 'HandleMessage' }, + ...defaultNodeOptions, + parentNode: 'shard-3', + extent: 'parent', + }, + { + id: 'shard-n', + type: 'output', + position: { x: 1.625 * multiplier, y: 40 }, + data: { label: 'Shard-N' }, + style: { width: `${multiplier * 2}px`, height: '80px' }, + ...defaultGroupOptions, + }, + { + id: 'shard-n-socket', + position: { x: 0.125 * multiplier, y: 20 }, + data: { label: 'webSocket' }, + ...defaultNodeOptions, + parentNode: 'shard-n', + extent: 'parent', + }, + { + id: 'shard-n-handleMessage', + position: { x: 1.125 * multiplier, y: 20 }, + data: { label: 'HandleMessage' }, + ...defaultNodeOptions, + parentNode: 'shard-n', + extent: 'parent', + }, + { + id: 'bot', + type: 'output', + position: { x: 4 * multiplier, y: 0 }, + data: { label: 'Bot' }, + ...defaultNodeOptions, + }, +] + +const initialEdges: Edge[] = [ + { id: 'd-g', source: 'discordGateway', target: 'gateway' }, + { id: 'g-b', source: 'gateway', target: 'bot' }, + { id: 'b-y', source: 'bot', target: 'yourCode' }, + { id: 'y-r', source: 'yourCode', target: 'rest' }, + { id: 'r-d', source: 'rest', target: 'discordApiGateway' }, + { + id: 'baseLine-1', + source: 'baseLineNode-1', + target: 'baseLineNode-2', + style: { stroke: 'blue', strokeDasharray: 20 }, + animated: false, + }, + { + id: 'baseLine-2', + source: 'baseLineNode-3', + target: 'baseLineNode-4', + style: { stroke: 'blue', strokeDasharray: 20 }, + animated: false, + }, + { + id: 'baseLine-3', + source: 'baseLineNode-5', + target: 'baseLineNode-6', + style: { stroke: 'blue', strokeDasharray: 20 }, + animated: false, + }, + { + id: 'baseLine-4', + source: 'baseLineNode-7', + target: 'baseLineNode-8', + style: { stroke: 'blue', strokeDasharray: 20 }, + animated: false, + }, + { + id: 'd-shard-1', + source: 'discordGateway', + target: 'shard-1-socket', + zIndex: 100, + }, + { + id: 'd-shard-2', + source: 'discordGateway', + target: 'shard-2-socket', + zIndex: 100, + }, + { + id: 'd-shard-3', + source: 'discordGateway', + target: 'shard-3-socket', + zIndex: 100, + }, + { + id: 'd-shard-n', + source: 'discordGateway', + target: 'shard-n-socket', + zIndex: 100, + }, + { + id: 'shard-1-socket-handleMessage', + source: 'shard-1-socket', + target: 'shard-1-handleMessage', + zIndex: 100, + }, + { + id: 'shard-2-socket-handleMessage', + source: 'shard-2-socket', + target: 'shard-2-handleMessage', + zIndex: 100, + }, + { + id: 'shard-3-socket-handleMessage', + source: 'shard-3-socket', + target: 'shard-3-handleMessage', + zIndex: 100, + }, + { + id: 'shard-n-socket-handleMessage', + source: 'shard-n-socket', + target: 'shard-n-handleMessage', + zIndex: 100, + }, + { + id: 'shard-1-handleMessage-bot', + source: 'shard-1-handleMessage', + target: 'bot', + zIndex: 100, + }, + { + id: 'shard-2-handleMessage-bot', + source: 'shard-2-handleMessage', + target: 'bot', + zIndex: 100, + }, + { + id: 'shard-3-handleMessage-bot', + source: 'shard-3-handleMessage', + target: 'bot', + zIndex: 100, + }, + { + id: 'shard-n-handleMessage-bot', + source: 'shard-n-handleMessage', + target: 'bot', + zIndex: 100, + }, + { + id: 'gatewayManager-shard-1', + source: 'gatewayManager', + target: 'shard-1', + zIndex: 10, + }, + { + id: 'gatewayManager-shard-2', + source: 'gatewayManager', + target: 'shard-2', + zIndex: 100, + }, + { + id: 'gatewayManager-shard-3', + source: 'gatewayManager', + target: 'shard-3', + zIndex: 100, + }, + { + id: 'gatewayManager-shard-n', + source: 'gatewayManager', + target: 'shard-n', + zIndex: 100, + }, +] + +export default function FlowChart2() { + return ( + + ) +} diff --git a/website/src/components/architecture/FlowChart3.tsx b/website/src/components/architecture/FlowChart3.tsx new file mode 100644 index 000000000..52bbd2a49 --- /dev/null +++ b/website/src/components/architecture/FlowChart3.tsx @@ -0,0 +1,910 @@ +import React, { useEffect, useState } from 'react' +import ReactFlow, { + Background, + Controls, + Edge, + Handle, + Node, + NodeMouseHandler, + Position, + useEdgesState, + useNodesState, +} from 'reactflow' +import 'reactflow/dist/style.css' +import { + defaultNodeOptions, + height, + multiplier, + widthMultiplier, +} from './BaseFlowChart' + +const handlers: { + [index: string]: { + transformers: string[] + event: string + } +} = { + handleChannelCreate: { + transformers: ['transformers.channel'], + event: 'events.channelCreate', + }, + handleChannelDelete: { + transformers: ['transformers.channel', 'transformers.snowflake'], + event: 'events.channelDelete', + }, + handleChannelPinsUpdate: { + transformers: ['transformers.snowflake', 'transformers.snowflake'], + event: 'events.channelPinsUpdate', + }, + handleChannelUpdate: { + transformers: ['transformers.channel'], + event: 'events.channelUpdate', + }, + handleStageInstanceCreate: { + transformers: [ + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.snowflake', + ], + event: 'events.stageInstanceCreate', + }, + handleStageInstanceDelete: { + transformers: [ + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.snowflake', + ], + event: 'events.stageInstanceDelete', + }, + handleStageInstanceUpdate: { + transformers: [ + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.snowflake', + ], + event: 'events.stageInstanceUpdate', + }, + handleThreadCreate: { + transformers: ['transformers.channel'], + event: 'events.threadCreate', + }, + handleThreadDelete: { + transformers: ['transformers.channel'], + event: 'events.threadDelete', + }, + handleThreadListSync: { + transformers: [ + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.channel', + 'transformers.snowflake', + 'transformers.snowflake', + ], + event: undefined, + }, + handleThreadMembersUpdate: { + transformers: [ + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.threadMember', + 'transformers.snowflake', + ], + event: 'events.threadMembersUpdate', + }, + handleThreadMemberUpdate: { + transformers: ['transformers.snowflake', 'transformers.snowflake'], + event: 'events.threadMemberUpdate', + }, + handleThreadUpdate: { + transformers: ['transformers.channel'], + event: 'events.threadUpdate', + }, + handleGuildEmojisUpdate: { + transformers: ['transformers.snowflake', 'transformers.snowflake'], + event: 'events.guildEmojisUpdate', + }, + handleAutoModerationActionExecution: { + transformers: ['transformers.automodActionExecution'], + event: 'events.automodActionExecution', + }, + handleAutoModerationRuleCreate: { + transformers: ['transformers.automodRule'], + event: 'events.automodRuleCreate', + }, + handleAutoModerationRuleDelete: { + transformers: ['transformers.automodRule'], + event: 'events.automodRuleDelete', + }, + handleAutoModerationRuleUpdate: { + transformers: ['transformers.automodRule'], + event: 'events.automodRuleUpdate', + }, + handleGuildBanAdd: { + transformers: ['transformers.user', 'transformers.snowflake'], + event: 'events.guildBanAdd', + }, + handleGuildBanRemove: { + transformers: ['transformers.user', 'transformers.snowflake'], + event: 'events.guildBanRemove', + }, + handleGuildCreate: { + transformers: ['transformers.guild'], + event: 'events.guildCreate', + }, + handleGuildDelete: { + transformers: ['transformers.snowflake'], + event: 'events.guildDelete', + }, + handleGuildIntegrationsUpdate: { + transformers: ['transformers.snowflake'], + event: 'events.integrationUpdate', + }, + handleGuildUpdate: { + transformers: ['transformers.guild'], + event: 'events.guildUpdate', + }, + handleGuildScheduledEventCreate: { + transformers: ['transformers.scheduledEvent'], + event: 'events.scheduledEventCreate', + }, + handleGuildScheduledEventDelete: { + transformers: ['transformers.scheduledEvent'], + event: 'events.scheduledEventDelete', + }, + handleGuildScheduledEventUpdate: { + transformers: ['transformers.scheduledEvent'], + event: 'events.scheduledEventUpdate', + }, + handleGuildScheduledEventUserAdd: { + transformers: [ + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.snowflake', + ], + event: 'events.scheduledEventUserAdd', + }, + handleGuildScheduledEventUserRemove: { + transformers: [ + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.snowflake', + ], + event: 'events.scheduledEventUserRemove', + }, + handleIntegrationCreate: { + transformers: ['transformers.integration'], + event: 'events.integrationCreate', + }, + handleIntegrationDelete: { + transformers: [ + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.snowflake', + ], + event: 'events.integrationDelete', + }, + handleIntegrationUpdate: { + transformers: ['transformers.integration'], + event: 'events.integrationUpdate', + }, + handleInteractionCreate: { + transformers: ['transformers.snowflake', 'transformers.interaction'], + event: 'events.interactionCreate', + }, + handleInviteCreate: { + transformers: ['transformers.invite'], + event: 'events.inviteCreate', + }, + handleInviteDelete: { + transformers: ['transformers.snowflake', 'transformers.snowflake'], + event: 'events.inviteDelete', + }, + handleGuildMembersChunk: { + transformers: [ + 'transformers.snowflake', + 'transformers.member', + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.user', + 'transformers.activity', + ], + event: undefined, + }, + handleGuildMemberAdd: { + transformers: [ + 'transformers.snowflake', + 'transformers.user', + 'transformers.member', + ], + event: 'events.guildMemberAdd', + }, + handleGuildMemberRemove: { + transformers: ['transformers.snowflake', 'transformers.user'], + event: 'events.guildMemberRemove', + }, + handleGuildMemberUpdate: { + transformers: [ + 'transformers.user', + 'transformers.member', + 'transformers.snowflake', + ], + event: 'events.guildMemberUpdate', + }, + handleMessageCreate: { + transformers: ['transformers.message'], + event: 'events.messageCreate', + }, + handleMessageDelete: { + transformers: [ + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.snowflake', + ], + event: 'events.messageDelete', + }, + handleMessageDeleteBulk: { + transformers: [ + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.snowflake', + ], + event: 'events.messageDeleteBulk', + }, + handleMessageReactionAdd: { + transformers: [ + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.member', + 'transformers.user', + 'transformers.emoji', + ], + event: 'events.reactionAdd', + }, + handleMessageReactionRemove: { + transformers: [ + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.emoji', + ], + event: 'events.reactionRemove', + }, + handleMessageReactionRemoveAll: { + transformers: [ + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.snowflake', + ], + event: 'events.reactionRemoveAll', + }, + handleMessageReactionRemoveEmoji: { + transformers: [ + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.emoji', + ], + event: 'events.reactionRemoveEmoji', + }, + handleMessageUpdate: { + transformers: ['transformers.message'], + event: 'events.messageUpdate', + }, + handlePresenceUpdate: { + transformers: ['transformers.presence'], + event: 'events.presenceUpdate', + }, + handleReady: { + transformers: [ + 'transformers.user', + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.snowflake', + ], + event: 'events.ready', + }, + handleTypingStart: { + transformers: [ + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.snowflake', + 'transformers.member', + ], + event: 'events.typingStart', + }, + handleUserUpdate: { + transformers: ['transformers.user'], + event: 'events.botUpdate', + }, + handleGuildRoleCreate: { + transformers: ['transformers.role', 'transformers.snowflake'], + event: 'events.roleCreate', + }, + handleGuildRoleDelete: { + transformers: ['transformers.snowflake', 'transformers.snowflake'], + event: 'events.roleDelete', + }, + handleGuildRoleUpdate: { + transformers: ['transformers.role', 'transformers.snowflake'], + event: 'events.roleUpdate', + }, + handleVoiceServerUpdate: { + transformers: ['transformers.snowflake'], + event: 'events.voiceServerUpdate', + }, + handleVoiceStateUpdate: { + transformers: ['transformers.snowflake', 'transformers.voiceState'], + event: 'events.voiceStateUpdate', + }, + handleWebhooksUpdate: { + transformers: ['transformers.snowflake', 'transformers.snowflake'], + event: 'events.webhooksUpdate', + }, +} + +export default function FlowChart({ + handlerFilter = (handler: string) => true, +}) { + function getWindowDimensions() { + const { innerWidth: width, innerHeight: height } = window + return { + width, + height, + } + } + + const [windowDimensions, setWindowDimensions] = useState( + getWindowDimensions(), + ) + + useEffect(() => { + function handleResize() { + setWindowDimensions(getWindowDimensions()) + } + + window.addEventListener('resize', handleResize) + return () => window.removeEventListener('resize', handleResize) + }, []) + + const transformers = [] + + const events = [] + + const initialNodes: Node[] = [ + { + id: 'baseNode-gateway', + type: 'input', + position: { x: 0 * multiplier, y: 0 }, + data: { label: 'gateway' }, + ...defaultNodeOptions, + }, + { + id: 'baseNode-yourCode', + type: 'output', + position: { x: 5 * multiplier, y: 0 }, + data: { label: 'Your Code' }, + ...defaultNodeOptions, + }, + { + id: 'baseLineNode-1', + type: 'baseLineNode', + position: { x: 0.85 * multiplier, y: 170 }, + data: {}, + }, + { + id: 'baseLineNode-2', + type: 'baseLineNode', + position: { x: 0.85 * multiplier, y: -130 }, + data: {}, + }, + { + id: 'baseLineNode-3', + type: 'baseLineNode', + position: { x: 4.85 * multiplier, y: 170 }, + data: {}, + }, + { + id: 'baseLineNode-4', + type: 'baseLineNode', + position: { x: 4.85 * multiplier, y: -130 }, + data: {}, + }, + { + id: 'baseLineNodeText-1', + type: 'baseLineNodeText', + position: { x: 0 * multiplier, y: -130 }, + data: { label: 'Gateway' }, + }, + { + id: 'baseLineNodeText-2', + type: 'baseLineNodeText', + position: { x: 1 * multiplier, y: -130 }, + data: { label: 'Bot' }, + }, + { + id: 'baseLineNodeText-3', + type: 'baseLineNodeText', + position: { x: 5 * multiplier, y: -130 }, + data: { label: 'Your Code' }, + }, + { + id: 'baseNode-handleDiscordPayload', + position: { x: 1 * multiplier, y: 0 }, + data: { label: 'Handle discord payload' }, + ...defaultNodeOptions, + }, + ] + + const initialEdges: Edge[] = [ + { + id: 'baseEdge-1', + source: 'baseNode-gateway', + target: 'baseNode-handleDiscordPayload', + }, + { id: 'baseEdge-3', source: 'baseNode-g-1', target: 'baseNode-g-2' }, + { + id: 'baseEdge-4', + source: 'baseNode-g-2', + target: 'baseNode-handleDiscordPayload', + }, + { + id: 'baseLine-1', + source: 'baseLineNode-1', + target: 'baseLineNode-2', + style: { stroke: 'blue', strokeDasharray: 20 }, + }, + { + id: 'baseLine-2', + source: 'baseLineNode-3', + target: 'baseLineNode-4', + style: { stroke: 'blue', strokeDasharray: 20 }, + }, + ] + + //@ts-ignore + for (const [index, handler] of Object.keys(handlers) + .filter(handlerFilter) + .entries()) { + initialNodes.push({ + id: handler, + position: { + x: 2 * multiplier, + y: + index * (height + 10) - + Object.keys(handlers).filter(handlerFilter).length * + ((height + 10) / 2) + + height / 2, + }, + data: { label: handler.slice(6) }, + ...defaultNodeOptions, + }) + initialEdges.push({ + id: `handleDiscordPayload-${handler}`, + source: 'baseNode-handleDiscordPayload', + target: handler, + }) + if ( + !events.find(e => e === handlers[handler].event) && + handlers[handler].event + ) { + events.push(handlers[handler].event) + initialEdges.push({ + id: `${handlers[handler].event}-yourCode`, + source: handlers[handler].event, + target: 'baseNode-yourCode', + }) + } + for (const transformer of handlers[handler].transformers) { + if (!transformers.find(t => t === transformer) && transformer) + transformers.push(transformer) + if ( + !initialEdges.find(edge => edge.id === `${handler}-${transformer}`) && + transformer + ) { + initialEdges.push({ + id: `${handler}-${transformer}`, + source: handler, + target: transformer, + }) + } + if ( + !initialEdges.find( + edge => edge.id === `${transformer}-${handlers[handler].event}`, + ) && + handlers[handler].event + ) { + initialEdges.push({ + id: `${transformer}-${handlers[handler].event}`, + source: transformer, + target: handlers[handler].event, + }) + } + } + } + + //@ts-ignore + for (const [index, transformer] of transformers.entries()) { + initialNodes.push({ + id: transformer, + position: { + x: 3 * multiplier, + y: + index * (height + 10) - + transformers.length * ((height + 10) / 2) + + height / 2, + }, + data: { label: transformer.slice(13) }, + ...defaultNodeOptions, + }) + } + + //@ts-ignore + for (const [index, event] of events.entries()) { + initialNodes.push({ + id: event, + position: { + x: 4 * multiplier, + y: + index * (height + 10) - + events.length * ((height + 10) / 2) + + height / 2, + }, + data: { label: event.slice(7) }, + ...defaultNodeOptions, + }) + } + + initialNodes.unshift( + { + id: 'handlers', + type: 'group', + position: { + x: 1.925 * multiplier, + y: + -Object.keys(handlers).filter(handlerFilter).length * + ((height + 10) / 2) - + 45, + }, + data: { label: '' }, + style: { + height: `${ + Object.keys(handlers).filter(handlerFilter).length * (height + 10) + + 75 + }px`, + width: `${multiplier * 0.9}px`, + borderColor: 'rgba(0,0,0,0.25)', + }, + draggable: false, + }, + { + id: 'baseLineNodeText-4', + type: 'baseLineNodeText', + position: { + x: 2 * multiplier, + y: + -Object.keys(handlers).filter(handlerFilter).length * + ((height + 10) / 2) - + (height + 10) / 2, + }, + data: { label: 'Handlers' }, + draggable: false, + }, + { + id: 'transformers', + type: 'group', + position: { + x: 2.925 * multiplier, + y: -transformers.length * ((height + 10) / 2) - 45, + }, + data: { label: '' }, + style: { + height: `${transformers.length * (height + 10) + 75}px`, + width: `${multiplier * 0.9}px`, + borderColor: 'rgba(0,0,0,0.25)', + }, + draggable: false, + }, + { + id: 'baseLineNodeText-5', + type: 'baseLineNodeText', + position: { + x: 3 * multiplier, + y: -transformers.length * ((height + 10) / 2) - (height + 10) / 2, + }, + data: { label: 'Transformers' }, + draggable: false, + }, + { + id: 'events', + type: 'group', + position: { + x: 3.925 * multiplier, + y: -events.length * ((height + 10) / 2) - 45, + }, + data: { label: '' }, + style: { + height: `${events.length * (height + 10) + 75}px`, + width: `${multiplier * 0.9}px`, + borderColor: 'rgba(0,0,0,0.25)', + }, + draggable: false, + }, + { + id: 'baseLineNodeText-6', + type: 'baseLineNodeText', + position: { + x: 4 * multiplier, + y: -events.length * ((height + 10) / 2) - (height + 10) / 2, + }, + data: { label: 'Event' }, + draggable: false, + }, + ) + + const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes) + const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges) + const [handlerIndex, setHandlerIndex] = useState(0) + const [userClick, setUserClick] = useState(false) + + const nodeMouseHandler: NodeMouseHandler = (_, node, userTrigger = true) => { + if (userTrigger) setUserClick(true) + if (node.id.split('-')[0] === 'baseNode') { + edges.forEach(e => { + if (e.id.startsWith('baseLine')) return + e.animated = true + e.style = { stroke: 'blue' } + }) + setEdges([...edges]) + return + } + if (Object.keys(handlers).find(h => handlers[h].event === node.id)) { + const handlerName = Object.keys(handlers).find( + h => handlers[h].event === node.id, + ) + const handler = handlers[handlerName] + edges.forEach(e => { + if (e.id.startsWith('baseLine')) return + if (e.id.split('-')[0] === 'baseEdge') { + e.animated = true + e.style = { stroke: 'blue' } + return + } + if ( + e.id.split('-')[0] === 'handleDiscordPayload' && + e.id.split('-')[1] === handlerName + ) { + e.animated = true + e.style = { stroke: 'blue' } + return + } + if ( + e.id.split('-')[0] === handlerName && + handler.transformers.includes(e.id.split('-')[1]) + ) { + e.animated = true + e.style = { stroke: 'blue' } + return + } + if ( + handler.transformers.includes(e.id.split('-')[0]) && + e.id.split('-')[1] === handler.event + ) { + e.animated = true + e.style = { stroke: 'blue' } + return + } + if ( + e.id.split('-')[0] === handler.event && + e.id.split('-')[1] === 'yourCode' + ) { + e.animated = true + e.style = { stroke: 'blue' } + return + } + e.animated = false + e.style = { opacity: 0.3 } + }) + setEdges([...edges]) + return + } + if ( + Object.keys(handlers).find(h => + handlers[h].transformers.includes(node.id), + ) + ) { + edges.forEach(e => { + if (e.id.startsWith('baseLine')) return + if (e.id.split('-')[0] === 'baseEdge') { + e.animated = true + e.style = { stroke: 'blue' } + return + } + if ( + e.id.split('-')[0] === 'handleDiscordPayload' && + Object.keys(handlers) + .filter(h => handlers[h].transformers.includes(node.id)) + .includes(e.id.split('-')[1]) + ) { + e.animated = true + e.style = { stroke: 'blue' } + return + } + if ( + Object.keys(handlers) + .filter(h => handlers[h].transformers.includes(node.id)) + .includes(e.id.split('-')[0]) && + e.id.split('-')[1] === node.id + ) { + e.animated = true + e.style = { stroke: 'blue' } + return + } + if (e.id.split('-')[0] === node.id) { + e.animated = true + e.style = { stroke: 'blue' } + return + } + e.animated = false + e.style = { opacity: 0.3 } + }) + setEdges([...edges]) + return + } + if (handlers[node.id]) { + const handler = handlers[node.id] + edges.forEach(e => { + if (e.id.startsWith('baseLine')) return + if (e.id.split('-')[0] === 'baseEdge') { + e.animated = true + e.style = { stroke: 'blue' } + return + } + if ( + e.id.split('-')[0] === 'handleDiscordPayload' && + e.id.split('-')[1] === node.id + ) { + e.animated = true + e.style = { stroke: 'blue' } + return + } + if ( + e.id.split('-')[0] === node.id && + handler.transformers.includes(e.id.split('-')[1]) + ) { + e.animated = true + e.style = { stroke: 'blue' } + return + } + if ( + handler.transformers.includes(e.id.split('-')[0]) && + e.id.split('-')[1] === handler.event + ) { + e.animated = true + e.style = { stroke: 'blue' } + return + } + if (e.id.split('-')[0] === handler.event) { + e.animated = true + e.style = { stroke: 'blue' } + return + } + e.animated = false + e.style = { opacity: 0.3 } + }) + setEdges([...edges]) + return + } + edges.forEach(e => { + if (e.id.startsWith('baseLine')) return + e.animated = false + e.style = {} + }) + setEdges([...edges]) + } + + useEffect(() => { + const interval = setInterval(() => { + const randomIndex = Math.round( + (Object.keys(handlers).filter(handlerFilter).length - 1) * + Math.random(), + ) + if (!userClick) { + nodeMouseHandler( + undefined, + { id: Object.keys(handlers).filter(handlerFilter)[randomIndex] }, + false, + ) + } + setHandlerIndex(randomIndex) + }, 1000) + return () => clearInterval(interval) + }, [userClick]) + + useEffect(() => { + if (userClick) { + const timeout = setTimeout(() => { + setUserClick(false) + }, 10000) + return () => clearTimeout(timeout) + } + }, [userClick]) + + return ( + <> +
= 997 + ? `${ + (100 * + ((windowDimensions.width - + 300 - + (windowDimensions.width >= 1620 + ? (windowDimensions.width - 1620) * 0.5 + : 0)) / + windowDimensions.width) - + 2) * + widthMultiplier + }vw` + : '95vw', + height: '50vh', + }} + > + { + //@ts-ignore + if (e.target.className === 'react-flow__pane') + nodeMouseHandler(e, { id: ' - ', data: { label: ' - ' } }) + }} + nodeTypes={{ + baseLineNode: () => ( +
+ + +
+ ), + baseLineNodeText: n => ( +
+

{n.data.label}

+
+ ), + }} + fitView + > + + +
+
+ + ) +} diff --git a/website/tutorial/amethyst/AmethystCollection.md b/website/tutorial/amethyst/AmethystCollection.md new file mode 100644 index 000000000..14e0ddf68 --- /dev/null +++ b/website/tutorial/amethyst/AmethystCollection.md @@ -0,0 +1,7 @@ +--- +sidebar_position: 5 +--- + +# Same as discord.js [collection][def] + +[def]: https://discord.js.org/#/docs/collection/main/class/Collection diff --git a/website/tutorial/amethyst/AmethystEmbed.md b/website/tutorial/amethyst/AmethystEmbed.md new file mode 100644 index 000000000..f654b38d6 --- /dev/null +++ b/website/tutorial/amethyst/AmethystEmbed.md @@ -0,0 +1,7 @@ +--- +sidebar_position: 4 +--- + +# Documentation [at][def] + +[def]: https://deno.land/x/amethyst@v4.3.4/mod.ts?s=AmethystEmbed diff --git a/website/tutorial/amethyst/_category_.json b/website/tutorial/amethyst/_category_.json new file mode 100644 index 000000000..b564ff089 --- /dev/null +++ b/website/tutorial/amethyst/_category_.json @@ -0,0 +1 @@ +{ "label": "Amethyst Framework", "position": 4 } diff --git a/website/tutorial/amethyst/client.md b/website/tutorial/amethyst/client.md new file mode 100644 index 000000000..cc2fba13d --- /dev/null +++ b/website/tutorial/amethyst/client.md @@ -0,0 +1,99 @@ +--- +sidebar_position: 2 +--- + +# Creating an client + +Let's review each choice and what it does. + +- `owners`, You may specify the proprietors of the bot using this. The inhibitors make use of this. +- `prefix`, The string a user should use at the beginning of their message to identify it as a command to the bot. Only + message commands can use this, and the parameter can be either a string or a function. +- `botMentionAsPrefix`, Determines whether a user's mention of a bot qualifies as a prefix. +- `ignoreBots`, Allow bots to execute commands. +- `defaultCooldown`, Defualt cooldown for all commands. +- `ignoreCooldown`, List of people who bypass cooldowns. +- `commandDir`, Path to the command directory used by the fileloader. +- `eventDir`, Path to the event directory used by the fileloader. +- `inhibitorDir`, Path to the inhibitor directory used by the fileloader. +- `prefixCaseSensitive`, Indicates whether or not the prefix is case-sensitive. +- `extras`, Extras that are used by your client, such as a database instance or a music player. + +## Client Extras + +When using discord.js we often do stuff like `client.musicplayer=player;` and in order to maintain this ease Amethyst +allows you to do `client.extras.musicplayer=player;`. + +NOTE: Typing will not work on `client.extras`. + +## Client Properties + +```ts + user: User; + events: AmethystEvents; + messageCollectors: AmethystCollection; + componentCollectors: AmethystCollection; + reactionCollectors: AmethystCollection; + runningTasks: runningTasks; + tasks: AmethystCollection; + category: AmethystCollection; + inhibitors: AmethystCollection< + string, + ( + bot: AmethystBot, + command: T, + options: { memberId?: bigint; channelId: bigint; guildId?: bigint } + ) => true | AmethystError + >; + owners?: bigint[]; + botMentionAsPrefix?: boolean; + prefixCaseSensitive?: boolean; + defaultCooldown?: CommandCooldown; + ignoreCooldown?: bigint[]; + guildOnly?: boolean; + messageQuotedArguments?: boolean; + ignoreBots?: boolean; + dmOnly?: boolean; + eventHandler: AmethystEventHandler; + extras: any; + prefix?: + | string + | string[] + | ((bot: AmethystBot, message: Message) => Async); + + on(name: string, callback: (...args: any) => unknown): void; + once(name: string, callback: (...args: any) => unknown): void; + + amethystUtils: { + awaitComponent( + messageId: bigint, + options?: ComponentCollectorOptions & { maxUsage?: number } + ): Promise, + awaitReaction( + messageId: bigint, + options?: ReactionCollectorOptions & { maxUsage?: number } + ): Promise, + awaitMessage( + memberId: bigint, + channelId: bigint, + options?: MessageCollectorOptions & { maxUsage?: number } + ): Promise, + createCommand(command: CommandOptions): void, + createCategory(category: CategoryOptions): void, + updateCategory(category: CategoryOptions): void, + createTask(task: AmethystTask): void, + clearTasks(): void, + createInhibitor( + name: string, + inhibitor: ( + bot: AmethystBot, + command: T, + options?: { memberId?: bigint; guildId?: bigint; channelId: bigint } + ) => true | AmethystError + ): void, + deleteInhibitor(name: string): void, + updateSlashCommands(): void, + } +``` + +## [Documentation](https://deno.land/x/amethyst@v4.2.0/mod.ts?s=AmethystBotOptions) diff --git a/website/tutorial/amethyst/exampleBot.md b/website/tutorial/amethyst/exampleBot.md new file mode 100644 index 000000000..92b036226 --- /dev/null +++ b/website/tutorial/amethyst/exampleBot.md @@ -0,0 +1,82 @@ +--- +sidebar_position: 3 +--- + +# Lets Create a simple bot in Node.js + +- **Step 1**: Create a typescript project with index.ts as main file. + +- **Step 2**: Installing packages. Install following packages. + +```bash +npm i @thereallonewolf/amethystframework +``` + +- **Step 3**: Create a index.ts file. + +- **Step 4**: Add following code in index.ts file, replacing TOKEN with your bot token. + +```ts +import { createBot, GatewayIntents, startBot } from 'discordeno' +import { enableCachePlugin, enableCacheSweepers } from 'discordeno/cache-plugin' +import { + AmethystBot, + Category, + Command, + Context, + enableAmethystPlugin, + Event, +} from '@thereallonewolf/amethystframework' + +let baseClient = createBot({ + token: 'TOKEN', + intents: + GatewayIntents.Guilds | + GatewayIntents.GuildMessages | + GatewayIntents.MessageContent, +}) + +//@ts-ignore +let client = enableAmethystPlugin(enableCachePlugin(baseClient), { + botMentionAsPrefix: true, + prefix: '!', //Can be a function or a string. + ignoreBots: false, +}) +enableCacheSweepers(client) + +startBot(client) + +@Category({ + name: 'general', + description: 'My general commands', + uniqueCommands: true, + default: '', //As all the commands are unique so no need to set the default command. +}) +export class General { + @Command({ + name: 'ping', + description: 'Pong!', + commandType: ['application', 'message'], + category: 'general', + args: [], + }) + async ping(bot: AmethystBot, ctx: Context) { + ctx.reply({ content: 'Pong!' }) + } + + @Event('ready') + async ready() { + console.log('I am ready!') + client.amethystUtils.updateSlashCommands() + } +} +``` + +- **Step 5**: Invite your bot and compile index.ts and run it. Then you can use `/general ping` or `!ping` + +- **Step 6**: Useful links: + +1. Command Options can be found + [here](https://github.com/AmethystFramework/framework/blob/master/src/types/commandOptions.ts). +2. Category Options [here](https://github.com/AmethystFramework/framework/blob/master/src/types/categoryOptions.ts) +3. Full [Documentation](https://deno.land/x/amethyst) diff --git a/website/tutorial/amethyst/intro.md b/website/tutorial/amethyst/intro.md new file mode 100644 index 000000000..174d5e428 --- /dev/null +++ b/website/tutorial/amethyst/intro.md @@ -0,0 +1,54 @@ +--- +sidebar_position: 1 +--- + +# Amethyst + +Amethyst is a [Discordeno](https://github.com/discordeno/discordeno) plugin that is incredibly robust and flexible. It +promotes standard practices and is geared at bigger bots. + +This framework is not for you if you cannot utilise Maps and Sets without reading them up. We presume you have a solid +foundation in typescript/javascript and dicord bots. + +[Documentation](https://deno.land/x/amethyst) + +## Features + +- Thanks to Amethyst's adaptability, you can change a lot of things and add features as you see appropriate. +- A developer may create slash or message interactions with Amethyst. +- Assistance with interactions, such as selection, built-in buttons, and more. +- Custom discord.js like event system without the event emitter. +- Explore more incredible features of our framework. + +## Why Amethyst? + +Amethyst makes use of the [Discordeno](https://github.com/discordeno/discordeno) plugin system to streamline your coding +process and help you get going more quickly. Support for message and slash commands that doesn't need altering any code. + +- Fully programmable + +- Easy to learn and utilise. + +- Conversion from Discord.js bots is simple. + +## Ideas + +The objective is to provide a framework that can be used with both Node.js and Deno while minimising transitions and +rewrites. + +- Easy to understand and use. +- Cover up complexity in the engine keeping the end user interface as simple as possible. + +## Future Updates + +Creation of discord setup wizards for commands like welcome, context menus and paginated messages. + +## Installation + +Deno: [link](https://deno.land/x/amethyst) + +Npm: + +```bash +npm i @thereallonewolf/amethystframework +``` diff --git a/website/tutorial/big-bot-guide/_category_.json b/website/tutorial/big-bot-guide/_category_.json new file mode 100644 index 000000000..3ff52b832 --- /dev/null +++ b/website/tutorial/big-bot-guide/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Big Bot Guide", + "position": 2 +} diff --git a/website/tutorial/big-bot-guide/cache.md b/website/tutorial/big-bot-guide/cache.md new file mode 100644 index 000000000..18302f8c0 --- /dev/null +++ b/website/tutorial/big-bot-guide/cache.md @@ -0,0 +1,119 @@ +--- +sidebar_position: 4 +sidebar_label: Step 3 - Cache +--- + +# Step 3: Standalone Cache Process + +The next part of this is going to be about making a standalone cache process. By now, you should have both a REST and a +Gateway process ready. Before, we start handling events we should build a Cache handler. + +## Why Use Standalone Cache Process? + +A standalone cache process allows you to retain cached data even after bot restarts. For example, if you are caching +member roles to track when a role was added or removed, you may want to cache the members. The question then comes to +play, when deciding where to keep your cache. Another reason to use this is, whether or not you are using a standalone +gateway process. + +- Start rest process +- Start event handler process (bot) +- Start gateway process. + - Guild create events arrive providing all the data needed to cache in the bot process. +- Restart event handler process(maybe for an update/reboot) + - You lost all guilds/channels/permissions etc and can not get them again without restarting gateway. This defeats the + entire point of the standalone gateway. + +If your cache is tied to the bot processes which is not tied to the gateway you lose all this info. The next thought is +to just keep the Cache entirely in the gateway process however, I do not like this personally however, should you desire +this you can do this as well. The reason I prefer not to do this is when your bot needs to make requests to your cache, +you do not want it occupying the thread for processing other gateway events arriving from discord. A separate cache +process makes it so it uses an entirely separate thread and will not slow down anything else. + +## Understand Cache Types + +When I use the term cache process, this is interchangeable with any similar term such as "custom cache", "redis cache", +"pgsql cache", etc... The fact is you can keep this "cache" anywhere. For this guide, we will implement a very simple +cache using pgsql. Feel free to modify this any way you like as advanced as you like. The point is Discordeno cache is +flexible enough to let you use anything for your Cache storage. + +## Setting Up The Cache + +This step is for you to create the base schema for your cache. For example, if you want to implement a pgsql or redis +cache perhaps you want to prepare the tables/schema. For this guide, we are just going to do a quick little hack to get +a custom cache working. + +Create a file in a path like `src/bot/cache/schema.sql` + +```sql +CREATE TABLE IF NOT EXISTS "users" ( + id bigint NOT NULL, + username text COLLATE pg_catalog."default" NOT NULL, + discriminator text COLLATE pg_catalog."default" NOT NULL, + bot boolean, + CONSTRAINT "users_pkey" PRIMARY KEY (id) +) +``` + +Note that you can cache only properties you want and leave all other properties that you won't use. + +Now that we have this schema ready for our users cache. Go ahead and repeat this for all other cache tables. + +Cache Tables: + +- users +- members +- guilds +- channels +- threads +- messages +- presences +- unavailableGuilds + +Once you are finished continue forward, for the purpose of keeping this guide short we wont cover each table. + +> You should also run this file to prepare your pgsql and have your pgsql database running by now. Or whatever, cache +> service you use. + +### Cache Handler + +Now we will initiate our cache service. This may be different for you based on your choice of cache type. Since we are +using PGSQL for our cache layer, we will now instantiate it. + +```ts +import { postgres } from '../../../deps.ts' + +// YOU CUSTOM PGSQL INFO GOES HERE +const DATABASE_USERNAME = '' +const DATABASE_PASSWORD = '' +const DATABASE_NAME = '' +const DATABASE_HOST = '' +const DATABASE_PORT = 8956 +const DATABASE_MAX = 20 + +export const psql = postgres({ + username: DATABASE_USERNAME, + password: DATABASE_PASSWORD, + database: DATABASE_NAME, + host: DATABASE_HOST, + port: DATABASE_PORT, + max: DATABASE_MAX, + /*onnotice: (data) => { + logger.psql(`${data.severity} ${bgBrightBlack(`[${data.code}| ${data.file}:${data.line}]`)}`, data.message); + },*/ + types: { + bigint: postgres.BigInt, + }, +}) +``` + +To use the PGSQL driver we are using in this guide you can insert this into your `deps.ts`. + +```ts +// @deno-types="https://denopkg.com/porsager/postgres@e2a8595d7aa8c3c838b83b9bca7b890c1707ad2c/types/index.d.ts" +export { default as postgres } from 'https://denopkg.com/porsager/postgres@e2a8595d7aa8c3c838b83b9bca7b890c1707ad2c/deno/lib/index.js' +``` + +> Note: Remember you can use any driver you like. For deno users we prefer to use this library for PGSQL because it is +> more stable and more performant. + +Now that the cache layer is ready, we can proceed to begin creating our bot. diff --git a/website/tutorial/big-bot-guide/events.md b/website/tutorial/big-bot-guide/events.md new file mode 100644 index 000000000..e6f78aa1b --- /dev/null +++ b/website/tutorial/big-bot-guide/events.md @@ -0,0 +1,261 @@ +--- +sidebar_position: 5 +sidebar_label: Step 4 - Event Handler +--- + +# Step 4: Creating Standalone Event Handler + +Now we are about to start working on the bot code itself. The last 3 steps should be completed by the time you reach +this. The event handler process will be listening for events from any number of gateway instances and be ready to handle +them. + +In this guide, we may use the term `Bot` or the term `event handler`, remember that these refer to the same thing. This +is your main bot code. + +## Why Use Standalone Event Handler Process? + +The standalone event handler is the portion of your bot code that you will be changing the most. The three previous +steps created processes that are intended to never be turned off. This process is designed to let you restart whenever +you wish and be incredibly quick to restart. Since we don't have the delay to start up shards anymore, your code becomes +reloaded instantly. + +## Creating Event Handlers + +Create a file path like `src/bot/mod.ts`. + +```ts +import { DISCORD_TOKEN } from '../../configs.ts' +import { Collection, createBot, Intents } from '../../deps.ts' +import { psql } from './cache/mod.ts' + +export const bot = createBot({ + token: DISCORD_TOKEN, + botId: 270010330782892032n, + intents: Intents.Guilds | Intents.GuildMessages, + events: { + messageCreate: function (bot, message) { + console.log('message arrived') + }, + }, +}) +``` + +Alright that was a lot of code. Now let's break it down little by little. + +### Understanding createBot() + +**Basic Keys** + +- `token` if you can't figure this out stop reading and find another guide please. Thanks. +- `botId` This is going to be your bot id. The reason we require this here is because we are going to set up a + standalone gateway process. With most other libs, they can fill this information using the READY event. However, since + our gateway is designed not to reboot, we are not going to get the READY event whenever we restart our bot. This means + we won't be able to fill this information later. Another method to get the id is to use the `token` but discord + developers have mentioned that this behavior is not documented and not supposed to be relied on to remain stable. Due + to these reasons, we chose to just require the bot id be passed here. +- `applicationId` is an optional choice if your bot is old and has a unique id different from it's bot id. +- `intents`: Provide the intents you like using a bitwise OR operation (eg. `Intents.Guilds | Intents.GuildsMessages`). + String form supports autocomplete and type safety. +- `events`: These are your event handler functions. When a MESSAGE_CREATE event arrives from Discord it will be + processed here. We will set up the routing to run these functions later in the guide but for now you can see how to + set it up. Note, you can create these functions in separate files and just import them here as you wish. + +## Using Your Cache + +Since we are using a standalone gateway, a custom cache is essentially required as explained in step 3 of this guide. +Here we'll have some basic functions to make use of the cache we created in step 3. + +```ts +const cache = { + /** Get a single item from the table */ + async get(key) { + return await psql`SELECT * FROM ${psql( + tables[table], + )} WHERE "id" = ${psql.types.bigint(key)}` + }, + /** Completely empty this table. */ + async clear() { + await psql`TRUNCATE TABLE ${psql(tables[table])}` + }, + /** Delete the data related to this key from table. */ + async delete(key) { + await psql`DELETE FROM ${psql( + tables[table], + )} WHERE "id" = ${psql.types.bigint(key)}` + return true + }, + /** Check if there is data assigned to this key. */ + async has(key) { + return Boolean( + await psql`SELECT 1 FROM ${psql( + tables[table], + )} WHERE "id" = ${psql.types.bigint(key)}`, + ) + }, + /** Check how many items are stored in this table. */ + async size() { + return (await psql`SELECT COUNT("id") FROM ${psql(tables[table])}`).count + }, + /** Store new data to this table. */ + async set(key, data) { + await psql`INSERT INTO ${psql(tables[table])} ${psql( + data, + ...Object.keys(data), + )}` + return true + }, + // THESE TWO ARE USELESS FOR CUSTOM CACHE BUT NEED TO SHUT UP TS ERRORS + async forEach(callback) {}, + async filter(callback) { + return new Collection() + }, +} +``` + +You can insert any code you desire for your cache system here. Since we were using PGSQL, we used sql queries to make +these requests. However, should you need to communicate to Redis or anything else of your choice, you can do so here. + +> Note: The .filter() and .forEach() methods are unnecessary and should not be used for your bot as they are not +> optimized for performance. These are made for smaller bot users who would not leave itoh alone and in order to please +> them itoh gave them their hearts desire! LMAO! + +## Customizing Internal Code + +One of the best parts about discordeno is the flexibility. In order to show this off, we will use the `user` example but +you can apply this to any part of the library. + +### Why Is Customizing Important? + +At large scale, every single property can become expensive to store in your cache. For example, if your bot does not +make use of a `channel.topic` why storing potentially millions of strings in your memory for something you never +need/user. This could save you potentially GBs of memory to just remove this one property. + +### Customizing Process + +First, let's create a file in some path like `src/bot/internals/mod.ts`. Note that we will create quite a few files +below simply to keep code cleaner and simpler, in expectation that it will grow more complex later. You can merge them +as you wish. + +```ts +import { Bot } from '../../../deps.ts' +import { customizeBotTransformers } from './transformers/mod.ts' + +export function customizeBotInternals(bot: Bot) { + bot = customizeBotTransformers(bot) + // ADD AS MANY MORE CUSTOMIZATIONS HERE AS YOU LIKE TO HANDLERS, HELPERS, UTILS ETC... + return bot +} +``` + +We also need to add another file now at `src/bot/internals/transformers/mod.ts` + +```ts +import { Bot } from '../../../../deps.ts' +import { customizeUserTransformer } from './user.ts' + +export function customizeBotTransformers(bot: Bot) { + bot = customizeUserTransformer(bot) + // ADD ANY MORE CUSTOM TRANSFORMERS HERE + return bot +} +``` + +One more file at `src/bot/internals/transformers/user.ts` + +```ts +import { Bot, DiscordenoUser, transformUser } from '../../../../deps.ts' + +export function customizeUserTransformer(bot: Bot) { + bot.transformers.user = function (bot, payload) { + // REMOVE USELESS PROPS OUR BOT DOESNT USE + const { + system, + locale, + verified, + email, + flags, + mfaEnabled, + premiumType, + publicFlags, + ...user + } = transformUser(bot, payload) + + // RETURN ONLY USEFUL PROPS WE NEED TO USE AND CACHE IF NECESSARY + return user as DiscordenoUser + } + + return bot +} +``` + +First we override the internal transformer for the `user` object. What's cool is the typings will be automatically +provided :) Next, we use the `transformUser` function from the lib itself to make it create the internal user version. +The reason I do this is so when I update the library and a new property is added or removed i can simply update and get +it. Should you desire maximum control you can remove this entirely and only have what you want no matter what discord +sends. Discordeno gives you the ability to stay in control. + +This method can be applied to any transformer, helper function, gateway event handler, util function or any part of the +library. Anything and everything is possible to override. You do NOT need to fork and modify the library ever and give +yourself a headache trying to maintain your fork with updates. + +## Handling Incoming Gateway Events + +Remember, this is a separate process we need to make sure we are listening to incoming events from our gateway +instances. Since we used http in our Gateway step, we can create an http listener here as well. + +Create a file in a path like `src/bot/gatewayEventsListener.ts` + +Now we should create a http listener, check for authorization in headers, run `bot.events.raw` and `bot.handlers[event]` + +```ts +import { DiscordGatewayPayload } from 'discordeno' +import { EVENT_HANDLER_PORT, REST_AUTHORIZATION } from '../../configs.ts' + +const server = Deno.listen({ port: EVENT_HANDLER_PORT }) + +// Connections to the server will be yielded up as an async iterable. +for await (const conn of server) { + // In order to not be blocking, we need to handle each connection individually + // in its own async function. + handleRequest(conn) +} + +async function handleRequest(conn: Deno.Conn) { + // This "upgrades" a network connection into an HTTP connection. + const httpConn = Deno.serveHttp(conn) + // Each request sent over the HTTP connection will be yielded as an async + // iterator from the HTTP connection. + for await (const requestEvent of httpConn) { + if ( + !REST_AUTHORIZATION || + REST_AUTHORIZATION !== requestEvent.request.headers.get('AUTHORIZATION') + ) { + return requestEvent.respondWith( + new Response(JSON.stringify({ error: 'Invalid authorization key.' }), { + status: 401, + }), + ) + } + + const json = (await requestEvent.request.json()) as { + message: DiscordGatewayPayload + shardId: number + } + + // Run raw event. + bot.events.raw(bot, json.message, json.shardId) + + if (json.message.t && json.message.t !== 'RESUMED') { + // When a guild or something isn't in cache this will fetch it before doing anything else. + if (!['READY', 'GUILD_LOADED_DD'].includes(json.message.t)) { + await bot.events.dispatchRequirements(bot, json.message, json.shardId) + } + + // Run event function provided in bot.events + bot.handlers[json.message.t]?.(bot, json.message, json.shardId) + } + + new Response(undefined, { status: 200 }) + } +} +``` diff --git a/website/tutorial/big-bot-guide/gateway.md b/website/tutorial/big-bot-guide/gateway.md new file mode 100644 index 000000000..1df34317d --- /dev/null +++ b/website/tutorial/big-bot-guide/gateway.md @@ -0,0 +1,1076 @@ +--- +sidebar_position: 3 +sidebar_label: Step 2 - Gateway +--- + +# Step 2: Creating A Standalone Gateway Process + +If you are reading this, you should have your REST process completed. We are going to need it here. This process will be +connecting to discord's websockets which will send you all the events. + +Before, we dive into how, here is a quick summary of why you will want a standalone gateway process. + +## Why Use Standalone REST Process? + +- **Zero Downtime Updates**: + + - Your bot can be updated in a matter of seconds. With normal sharding, you have to restart which also has to process + identifying all your shards with a 1/~5s rate limit. With WS handling moved to a proxy process, this allows you to + instantly get the bot code restarted without any concerns of delays. If you have a bot on 200,000 servers normally + this would mean a 20 minute delay to restart your bot if you made a small change and restarted. + +- **Zero Downtime Resharding**: + + - Discord stops letting your bot get added to new servers at certain points in time. For example, suppose you had + 150,000 servers running 150 shards. The maximum amount of servers your shards could hold is 150 \* 2500 = 375,000. + If your bot reaches this, it can no longer join new servers until it re-shards. + - DD proxy 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. + +- **Horizontal Scaling**: + + - The proxy system allows you to scale the bot horizontally. When you reach a huge size, you can either keep spending + more money to keep beefing up your server or you can buy several cheaper servers and scale horizontally. The proxy + means you can have WS handling on a completely separate system. + +- **No Loss Restarts**: + + - When you restart a bot without the proxy system, normally you would lose many events. Users may be using commands or + messages are sent that will not be filtered. As your bot's grow this number rises dramatically. Users may join who + wont get the auto-roles or any other actions your bot should take. With the proxy system, you can keep restarting + your bot and never lose any events. Events will be put into a queue while your bot is down(max size of queue is + customizable), once the bot is available the queue will begin processing all events. + +- **Controllers**: + + - The controller aspect gives you full control over everything inside the proxy. You can provide a function to simply + override the handler. For example, if you would like a certain function to do something different, instead of having + to fork and maintain your fork, you can just provide a function to override. + +- **Clustering With Workers**: + - Take full advantage of all your CPU cores by using workers to spread the load. Control how many shards per worker + and how many workers to maximize efficiency! + +## Creating Gateway Manager + +Create a file under some path like `src/gateway/mod.ts`. + +```ts +import { DISCORD_TOKEN, REST_AUTHORIZATION, REST_PORT } from '../../configs.ts' +import { BASE_URL, createRestManager } from '../../deps.ts' + +const rest = createRestManager({ + token: DISCORD_TOKEN, + secretKey: REST_AUTHORIZATION, + customUrl: `http://localhost:${REST_PORT}`, +}) +``` + +Throw another rest manager here which will be responsible for calling the main REST process we created in Step 1. This +will allow your gateway to communicate to the other process. Remember this is just to communicate outwards, this file +should not have the http listener. + +> Feel free to refactor and optimize this should you wish to move `const rest...` to a separate file and reuse in both +> steps. + +### Getting Gateway Bot Data + +Now we need to use this rest manager to call the api to get information about how to connect to discord's gateway for +your bot. + +```ts +import { routes } from '../../deps.ts' + +const rest = createRestManager({ + token: DISCORD_TOKEN, + secretKey: REST_AUTHORIZATION, + customUrl: `http://localhost:${REST_PORT}`, +}) + +// CALL THE REST PROCESS TO GET GATEWAY DATA +const gatewayBot = await rest + .runMethod(rest, 'GET', routes.GATEWAY_BOT()) + .then(res => ({ + url: res.url, + shards: res.shards, + sessionStartLimit: { + total: res.session_start_limit.total, + remaining: res.session_start_limit.remaining, + resetAfter: res.session_start_limit.reset_after, + maxConcurrency: res.session_start_limit.max_concurrency, + }, + })) +``` + +With this info, we can now create our gateway manager. + +### Understanding Gateway Manager + +```ts +import { INTENTS, SHARDS_PER_WORKER, TOTAL_WORKERS } from '../../configs.ts' + +const gateway = createGatewayManager({ + gatewayBot, + gatewayConfig: { + token: DISCORD_TOKEN, + intents: INTENTS, + }, + totalShards: gatewayBot.shards, + shardsPerWorker: SHARDS_PER_WORKER, + totalWorkers: TOTAL_WORKERS, + // debug: console.log, + // THIS WILL BE USED LATER IN WORKER SO LEAVE IT HERE + handleDiscordPayload: () => {}, +}) +``` + +#### Basic Keys + +- `EVENT_HANDLER_SECRET_KEY` is from your configs that will be used to make sure requests sent to your event handler + process are indeed from you. +- `DISCORD_TOKEN` if you can't figure this out, this guide isn't for you. Please find another. +- `INTENTS` pass in a number or a string of intents. Autocomplete/type-safety is provided for strings :) + +#### Powerful Keys + +If your bot is going to be run on one process, you can re-use the data that discord gave you to connect. + +- `totalShards`: is the maximum number of shards you want to use for connecting your bot. Should you think Discord is + not smart enough to recommend a good amount, use this to override their choice. Highly recommend just using theirs. +- `lastShardId`: is the last shard you want to connect in this process. + - Using a combination of `lastShardId` & `firstShardId`, you can create several processes or even several servers to + handle different amounts of shards should your bot get that big to require horizontal scaling. You can control how + many shards each gateway manager will be responsible for. +- `spawnShardDelay`: The delay in milliseconds to wait before spawning next shard. +- `shardsPerWorker`: The amount of shards to load per worker. Discussed in detail below. +- `totalWorkers`: The maximum amount of workers to use for your bot. + +#### Gateway Cache + +There is a few things that we cache in the gateway process directly, because sending them across the network is not +ideal. This is done to support custom cache functionality. + +- `guildIds`: Used for determining what type of GUILD_CREATE event is received. +- `loadingGuildIds`: Used for determining if all guilds have arrived when initially connecting. +- `editedMessages`: Used to prevent spam of events across the network. MESSAGE_UPDATE are an extremely heavy event. Any + embed or link that is in a message will unfurl triggerring a message update event. This is undesired behavior for 99% + of bots out there. If someone sends a message with 5 urls, in there you will get a MESSAGE_CREATE and 5 MESSAGE_UPDATE + events. If that user edits a single letter on it you now get 6 MESSAGE_UPDATE events, 1 for the content change and 5 + more for each url being unfurled. The editedMessages cache checks if the content of the message changed or not before + sending the event downstream. Override this behavior if you need different behavior. + +#### Gateway Method Overriding + +One of the benefits of Discordeno is that you can override/customize anything from the library. Should you desire to +change the logic in any method it is as simple as: + +```ts +// TYPINGS WILL BE AUTOMATICALLY PROVIDED +gateway.heartbeat = function (gateway, shardId, interval) { + // YOUR CUSTOM HANDLING CODE HERE +} +``` + +## Workers + +Now, we should take a minute here to talk about workers. Workers are just Clusters in Node.js + +When you have a big bot and you are processing millions of events, you need to speed up that processing. Keeping it in 1 +thread is not very nice since JavaScript is single threaded. This means it can only process 1 event at a time. With +workers, you can make it process several events at the same time. We mentioned the `shardsPerWorker` earlier. This +option was added to allow you to choose how many shards should be managed by each worker. + +When shards are spawned, they are triggered by a method on gateway: `tellWorkerToIdentify`, so we'll have to modify it +to create workers and send message: + +```ts +gateway.tellWorkerToIdentify = async ( + _gateway, + workerId, + shardId, + _bucketId, +) => { + let worker = workers.get(workerId) + if (!worker) { + worker = createWorker(workerId) + workers.set(workerId, worker) + } + + // TYPE TYPE WorkerMessage IS FROM WORKER FILE, DISCUSSED IN DETAIL BELOW + const identify: WorkerMessage = { + type: 'IDENTIFY_SHARD', + shardId, + } + + worker.postMessage(identify) +} +``` + +You can choose to replace the handler with any desired functionality you like. For example, should should you want to +create a new worker for each new workerId that appears and have that worker trigger the identify functionaly. How you +choose to handler workers is left in your care. + +Now that we've setup our initial gateway manager and added `tellWorkerToIdentify` to `gateway`, we need to do the rest +of the work: creating workers, spawning shards etc. + +```ts +import { EVENT_HANDLER_SECRET_KEY, EVENT_HANDLER_URL } from '../../configs.ts' +import { Worker } from 'worker_threads' +import { + WorkerCreateData, + WorkerGetShardInfo, + WorkerMessage, + WorkerShardInfo, + WorkerShardPayload, +} from './worker.js' + +// A COLLECTION OF WORKERS +const workers = new Collection() +const nonces = new Collection void>() + +function createWorker(workerId: number) { + const workerData: WorkerCreateData = { + intents: gateway.manager.gatewayConfig.intents ?? 0, + token: DISCORD_TOKEN, + // TODO: PUT THIS SEPARATELY. CAN USE MULTIPLE URLS IF YOU HAVE MULTIPLE BOT PROCESSES HANDLING DIFFERENT SHARDS' EVENTS + handlerUrls: [EVENT_HANDLER_URL], + handlerAuthorization: EVENT_HANDLER_SECRET_KEY, + path: './worker.ts', + totalShards: gateway.manager.totalShards, + workerId, + } + + const worker = new Worker('./worker.js', { + workerData, + }) + + worker.on('message', async (data: ManagerMessage) => { + switch (data.type) { + case 'REQUEST_IDENTIFY': { + await gateway.manager.requestIdentify(data.shardId) + + const allowIdentify: WorkerMessage = { + type: 'ALLOW_IDENTIFY', + shardId: data.shardId, + } + + worker.postMessage(allowIdentify) + + break + } + case 'NONCE_REPLY': { + nonces.get(data.nonce)?.(data.data) + } + } + }) + + return worker +} + +// TYPES WE USE +export type ManagerMessage = + | ManagerRequestIdentify + | ManagerNonceReply + +export type ManagerRequestIdentify = { + type: 'REQUEST_IDENTIFY' + shardId: number +} + +export type ManagerNonceReply = { + type: 'NONCE_REPLY' + nonce: string + data: T +} +``` + +## Spawning Shards + +Once you are ready and the gateway has been created as you desired, we can begin spawning the shards. + +```ts +gateway.spawnShards(gateway) +``` + +This code now handles creating gateway manager, creating workers, spawning shards and sending the info to each workers. +Next is creating a worker file to receive these info, connecting to gateway and sending the events to bot process. + +Here's the full code of `src/gateway/mod.ts`: + +```ts +import { + DISCORD_TOKEN, + EVENT_HANDLER_SECRET_KEY, + EVENT_HANDLER_URL, + INTENTS, + REST_AUTHORIZATION, + REST_PORT, + SHARDS_PER_WORKER, + TOTAL_WORKERS, +} from '../../configs.ts' +import { BASE_URL, createRestManager, routes } from '../../deps.ts' +import { Worker } from 'worker_threads' +import { + WorkerCreateData, + WorkerGetShardInfo, + WorkerMessage, + WorkerShardInfo, + WorkerShardPayload, +} from './worker.ts' + +const rest = createRestManager({ + token: DISCORD_TOKEN, + secretKey: REST_AUTHORIZATION, + customUrl: `http://localhost:${REST_PORT}`, +}) + +const rest = createRestManager({ + token: DISCORD_TOKEN, + secretKey: REST_AUTHORIZATION, + customUrl: `http://localhost:${REST_PORT}`, +}) + +// CALL THE REST PROCESS TO GET GATEWAY DATA +const gatewayBot = await rest + .runMethod(rest, 'GET', routes.GATEWAY_BOT()) + .then(res => ({ + url: res.url, + shards: res.shards, + sessionStartLimit: { + total: res.session_start_limit.total, + remaining: res.session_start_limit.remaining, + resetAfter: res.session_start_limit.reset_after, + maxConcurrency: res.session_start_limit.max_concurrency, + }, + })) + +const gateway = createGatewayManager({ + gatewayBot, + gatewayConfig: { + token: DISCORD_TOKEN, + intents: INTENTS, + }, + totalShards: gatewayBot.shards, + shardsPerWorker: SHARDS_PER_WORKER, + totalWorkers: TOTAL_WORKERS, + // debug: console.log, + handleDiscordPayload: () => {}, + tellWorkerToIdentify: async (_gateway, workerId, shardId, _bucketId) => { + let worker = workers.get(workerId) + if (!worker) { + worker = createWorker(workerId) + workers.set(workerId, worker) + } + + // TYPE TYPE WorkerMessage IS FROM WORKER FILE, DISCUSSED IN DETAIL BELOW + const identify: WorkerMessage = { + type: 'IDENTIFY_SHARD', + shardId, + } + + worker.postMessage(identify) + }, +}) + +// A COLLECTION OF WORKERS +const workers = new Collection() +const nonces = new Collection void>() + +function createWorker(workerId: number) { + const workerData: WorkerCreateData = { + intents: gateway.manager.gatewayConfig.intents ?? 0, + token: DISCORD_TOKEN, + handlerUrls: [EVENT_HANDLER_URL], + handlerAuthorization: EVENT_HANDLER_SECRET_KEY, + path: './worker.ts', + totalShards: gateway.manager.totalShards, + workerId, + } + + const worker = new Worker('./worker.ts', { + workerData, + }) + + worker.on('message', async (data: ManagerMessage) => { + switch (data.type) { + case 'REQUEST_IDENTIFY': { + await gateway.manager.requestIdentify(data.shardId) + + const allowIdentify: WorkerMessage = { + type: 'ALLOW_IDENTIFY', + shardId: data.shardId, + } + + worker.postMessage(allowIdentify) + + break + } + case 'NONCE_REPLY': { + nonces.get(data.nonce)?.(data.data) + } + } + }) + + return worker +} + +// TYPES WE USE +export type ManagerMessage = + | ManagerRequestIdentify + | ManagerNonceReply + +export type ManagerRequestIdentify = { + type: 'REQUEST_IDENTIFY' + shardId: number +} + +export type ManagerNonceReply = { + type: 'NONCE_REPLY' + nonce: string + data: T +} + +// SPAWN SHARDS INTO WORKERS +gateway.spawnShards() +``` + +## Worker File + +Now that we've handled creating gateway, workers, we need to create a worker file to identify, receive gateway events +and send them to bot process. + +Create a file in a path like `src/gateway/worker.ts`. + +Now we'll have to create a Shard Manager, this is what will handle identifying, receiving events. + +```ts +import { createShardManager } from 'discordeno' +import { parentPort, workerData } from 'worker_threads' + +if (!parentPort) { + throw new Error('Parent port is null') +} + +// THE DATA WE GET FROM GATEWAY FILE +const script: WorkerCreateData = workerData + +const identifyPromises = new Map void>() + +const manager = createShardManager({ + gatewayConfig: { + intents: script.intents, + token: script.token, + }, + shardIds: [], + totalShards: script.totalShards, + // WE WILL COVER THESE TWO FUNCTIONS IN LATER PART OF THE GUIDE, FOR NOW, LEAVE IT THIS WAY + handleMessage: () => {}, + requestIdentify: async () => {}, +}) +``` + +The above code only creates a shard manager, we now have 3 more things to do: + +- Listening to gateway process, sending events received to respective shards in the manager. +- Handling Discord Payloads. +- Requesting Identify. + +## Sending events from Gateway to Shards in Manager + +In order for the shards to receive events and send to bot process, we need to receive the event payloads from gateway +first, we can do this by using `message` event in `parentPort` like shown below: + +```ts +import { Shard } from 'discordeno' + +function buildShardInfo(shard: Shard): WorkerShardInfo { + return { + workerId: script.workerId, + shardId: shard.id, + rtt: shard.heart.rtt || -1, + state: shard.state, + } +} + +parentPort.on('message', async (data: WorkerMessage) => { + switch (data.type) { + // Gateway sends IDENTIFY_SHARD in gateway.tellWorkerToIdentify + case 'IDENTIFY_SHARD': { + await manager.identify(data.shardId) + + break + } + // Gateway sends ALLOW_IDENTIFY when worker requests to identify + case 'ALLOW_IDENTIFY': { + identifyPromises.get(data.shardId)?.() + identifyPromises.delete(data.shardId) + + break + } + // Gateway sends SHARD_PAYLOAD for every events it receives from Discord + case 'SHARD_PAYLOAD': { + manager.shards.get(data.shardId)?.send(data.data) + + break + } + // Send shard info if gateway sends GET_SHARD_INFO + case 'GET_SHARD_INFO': { + const infos = manager.shards.map(buildShardInfo) + + parentPort?.postMessage({ + type: 'NONCE_REPLY', + nonce: data.nonce, + data: infos, + }) + } + } +}) +``` + +Now TypeScript will error because of missing types, add these to your code: + +```ts +import { ShardSocketRequest, ShardState } from 'discordeno' + +export type WorkerMessage = + | WorkerIdentifyShard + | WorkerAllowIdentify + | WorkerShardPayload + | WorkerGetShardInfo + +export type WorkerIdentifyShard = { + type: 'IDENTIFY_SHARD' + shardId: number +} + +export type WorkerAllowIdentify = { + type: 'ALLOW_IDENTIFY' + shardId: number +} + +export type WorkerShardPayload = { + type: 'SHARD_PAYLOAD' + shardId: number + data: ShardSocketRequest +} + +export type WorkerGetShardInfo = { + type: 'GET_SHARD_INFO' + nonce: string +} + +export type WorkerCreateData = { + intents: number + token: string + handlerUrls: string[] + handlerAuthorization: string + path: string + totalShards: number + workerId: number +} + +export type WorkerShardInfo = { + workerId: number + shardId: number + rtt: number + state: ShardState +} +``` + +## Handling Discord Payloads + +One of the big things we didn't cover yet is the handler for discord payloads. This is the main sauce of your worker +process here. This is going to take the events that the gateway manager sent and send it to your event handler. How you +wish to communicate with your event handler is up to you. For this guide, we will use http, but you can replace that +with anything you like. + +```ts +manager.createShardOptions.handleMessage = async (shard, message) => { + const url = script.handlerUrls[shard.id % script.handlerUrls.length] + if (!url) return console.error('ERROR: NO URL FOUND TO SEND MESSAGE') + + await fetch(url, { + method: 'POST', + body: JSON.stringify({ message, shardId: shard.id }), + headers: { + 'Content-Type': 'application/json', + Authorization: script.handlerAuthorization, + }, + }).catch(error => console.error(error)) +} +``` + +You can change this function to use a WS or any form of communication you prefer to use to send this to your event +handler. + +This is also the place where you make use of the [Gateway Cache](#gateway-cache) we mentioned earlier (`guildIds`, +`loadingGuildIds`, `editedMessages`). + +## Gateway Queue + +One thing we can add on here, which you will find already done in the template if you are using it. However, it is still +good to read this to learn and understand the logic behind it. When you need a downtime for whatever reason, you can +create a queue like system to avoid any missed events. Let's create a simple queue. If it errors, assuming something +like the bot event listener process is down for whatever reason, the `.catch` in `fetch` will run adding this event to +the queue to try again in one second by calling the `handleQueue` function. + +```ts +.catch(() => { + // IF FAILED TRY TO QUEUE MAYBE LISTENER IS DOWN + if (message.t === "INTERACTION_CREATE") handleInteractionQueueing(message, shard.id); + else queue.events.push({ shardId: shard.id, message }); + + setTimeout(handleQueue, 1000); +}); +``` + +Now TypeScript will probably throw some errors at your face, so let's fix those real quick. Create an object that will +hold the queue of events for our gateway. + +```ts +import { DiscordGatewayPayload } from 'discordeno' + +const queue: GatewayQueue = { + processing: false, + events: [], +} + +export interface QueuedEvent { + message: DiscordGatewayPayload + shardId: number +} + +export interface GatewayQueue { + processing: boolean + events: QueuedEvent[] +} + +async function handleQueue() { + // PLACEHOLDER FUNCTION THAT WILL HANDLE PROCESSING THE QUEUE +} + +async function handleInteractionQueueing( + message: DiscordGatewayPayload, + shardId: number, +) { + // PLACEHOLDER FUNCTION +} +``` + +Alrighty, since TypeScript stopped being annoying, let's continue. Next, we should make sure to avoid fetching when the +queue is already processing or has events queued up. This will help us preserve the order of events in the queue. + +```ts +handleMessage: async function (shard, message) { +// IF QUEUE IS RUNNING JUST ADD TO QUEUE +if (queue.processing) { + if (message.t === "INTERACTION_CREATE") return handleInteractionQueueing(message, shard.id); + + return queue.events.push({ shardId: shard.id, message }); +} + +await fetch(EVENT_HANDLER_URL, { +``` + +Typescript must be at it again so let's shut it up again. Keep in mind that we are handling interaction events +separately because they require a response within 3 seconds or they will become invalid. In this function first we +automatically respond to the ones that can not be deferred. For the interactions that can be deferred, we will simply +defer them and add this event to the queue. + +```ts +import { + DiscordInteraction, + InteractionResponseTypes, + InteractionTypes, + routes, +} from 'discordeno' +import { BOT_SERVER_INVITE_CODE } from '../../configs.ts' + +async function handleInteractionQueueing( + message: DiscordGatewayPayload, + shardId: number, +) { + if (message.t !== 'INTERACTION_CREATE') return + + const interaction = message.d as DiscordInteraction + // IF THIS INTERACTION IS NOT DEFERABLE + if ( + [ + InteractionTypes.ModalSubmit, + InteractionTypes.ApplicationCommandAutocomplete, + ].includes(interaction.type) + ) { + return await rest.runMethod( + rest, + 'POST', + routes.INTERACTION_ID_TOKEN(BigInt(interaction.id), interaction.token), + { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: { + content: `The bot is having a temporary issue, please try again or contact us at https://discord.gg/${BOT_SERVER_INVITE_CODE}`, + }, + }, + ) + } + + await rest.runMethod( + rest, + 'POST', + endpoints.INTERACTION_ID_TOKEN(BigInt(interaction.id), interaction.token), + { + // MESSAGE COMPONENTS NEED SPECIAL DEFER + type: + InteractionTypes.MessageComponent === interaction.type + ? InteractionResponseTypes.DeferredUpdateMessage + : InteractionResponseTypes.DeferredChannelMessageWithSource, + }, + ) + + // ADD EVENT TO QUEUE + queue.events.push({ shardId, message }) +} +``` + +Oh no, TypeScript is at it again. We need to make a REST manager so that our gateway proxy can communicate with our REST +proxy. We then can make use of it to send a POST request. + +```ts +const rest = createRestManager({ + token: DISCORD_TOKEN, + secretKey: REST_AUTHORIZATION, + customUrl: REST_URL, +}) +``` + +So now there is only one thing left the `handleQueue` function. First we get the first item from the queue using +`.shift()`. Then we check to see if that item exists. If it does not exist, we mark the queue as no longer processing +and cancel out. However, if it does exist, we send a fetch request to the bot event handler process. In the `.catch()` +we will add this event back in to the start of the queue in case the bot is still down. Finally we call this function +again to run the next item in the queue. + +```ts +async function handleQueue() { + const event = queue.events.shift() + // QUEUE IS EMPTY + if (!event) { + console.log('GATEWAY QUEUE ENDING') + queue.processing = false + return + } + + await fetch(EVENT_HANDLER_URL, { + headers: { + Authorization: EVENT_HANDLER_SECRET_KEY, + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify({ + shardId: event.shardId, + message: event.message, + }), + }) + .then(res => { + res.text() + handleQueue() + }) + .catch(() => { + // EVENT HANDLER STILL NOT ACCEPTING REQUEST. SO ADD BACK TO QUEUE + queue.events.unshift(event) + setTimeout(handleQueue, 1000) + }) +} +``` + +## Requesting Identify + +We need to request identify in order to trigger initial handshake with the gateway, we'll use `manager.requestIdentify` +to do this. + +```ts +import { ManagerMessage } from './mod.ts' + +manager.requestIdentify = async function (shardId: number): Promise { + return await new Promise(resolve => { + identifyPromises.set(shardId, resolve) + + const identifyRequest: ManagerMessage = { + type: 'REQUEST_IDENTIFY', + shardId, + } + + parentPort?.postMessage(identifyRequest) + }) +} +``` + +That's all, you've now setup your gateway and worker. Here's the full code of `src/gateway/worker.ts`: + +```ts +import { + createRestManager, + createShardManager, + DiscordGatewayPayload, + DiscordInteraction, + InteractionResponseTypes, + InteractionTypes, + routes, + Shard, + ShardSocketRequest, + ShardState, +} from 'discordeno' +import { parentPort, workerData } from 'worker_threads' +import { ManagerMessage } from './mod' +import { + BOT_SERVER_INVITE_CODE, + DISCORD_TOKEN, + EVENT_HANDLER_SECRET_KEY, + EVENT_HANDLER_URL, + REST_AUTHORIZATION, + REST_URL, +} from '../../configs.ts' + +if (!parentPort) { + throw new Error('Parent port is null') +} + +// THE DATA WE GET FROM GATEWAY FILE +const script: WorkerCreateData = workerData + +const identifyPromises = new Map void>() + +const rest = createRestManager({ + token: DISCORD_TOKEN, + secretKey: REST_AUTHORIZATION, + customUrl: REST_URL, +}) + +const manager = createShardManager({ + gatewayConfig: { + intents: script.intents, + token: script.token, + }, + shardIds: [], + totalShards: script.totalShards, + handleMessage: async (shard, message) => { + const url = script.handlerUrls[shard.id % script.handlerUrls.length] + if (!url) return console.error('ERROR: NO URL FOUND TO SEND MESSAGE') + + await fetch(url, { + method: 'POST', + body: JSON.stringify({ message, shardId: shard.id }), + headers: { + 'Content-Type': 'application/json', + Authorization: script.handlerAuthorization, + }, + }).catch(() => { + // IF FAILED TRY TO QUEUE MAYBE LISTENER IS DOWN + if (message.t === 'INTERACTION_CREATE') + handleInteractionQueueing(message, shard.id) + else queue.events.push({ shardId: shard.id, message }) + + setTimeout(handleQueue, 1000) + }) + }, + requestIdentify: async function (shardId: number): Promise { + return await new Promise(resolve => { + identifyPromises.set(shardId, resolve) + + const identifyRequest: ManagerMessage = { + type: 'REQUEST_IDENTIFY', + shardId, + } + + parentPort?.postMessage(identifyRequest) + }) + }, +}) + +function buildShardInfo(shard: Shard): WorkerShardInfo { + return { + workerId: script.workerId, + shardId: shard.id, + rtt: shard.heart.rtt || -1, + state: shard.state, + } +} + +parentPort.on('message', async (data: WorkerMessage) => { + switch (data.type) { + // Gateway sends IDENTIFY_SHARD in gateway.tellWorkerToIdentify + case 'IDENTIFY_SHARD': { + await manager.identify(data.shardId) + + break + } + // Gateway sends ALLOW_IDENTIFY when worker requests to identify + case 'ALLOW_IDENTIFY': { + identifyPromises.get(data.shardId)?.() + identifyPromises.delete(data.shardId) + + break + } + // Gateway sends SHARD_PAYLOAD for every events it receives from Discord + case 'SHARD_PAYLOAD': { + manager.shards.get(data.shardId)?.send(data.data) + + break + } + // Send shard info if gateway sends GET_SHARD_INFO + case 'GET_SHARD_INFO': { + const infos = manager.shards.map(buildShardInfo) + + parentPort?.postMessage({ + type: 'NONCE_REPLY', + nonce: data.nonce, + data: infos, + }) + } + } +}) + +const queue: GatewayQueue = { + processing: false, + events: [], +} + +async function handleQueue() { + const event = queue.events.shift() + // QUEUE IS EMPTY + if (!event) { + console.log('GATEWAY QUEUE ENDING') + queue.processing = false + return + } + + await fetch(EVENT_HANDLER_URL, { + headers: { + Authorization: EVENT_HANDLER_SECRET_KEY, + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify({ + shardId: event.shardId, + message: event.message, + }), + }) + .then(res => { + res.text() + handleQueue() + }) + .catch(() => { + // EVENT HANDLER STILL NOT ACCEPTING REQUEST. SO ADD BACK TO QUEUE + queue.events.unshift(event) + setTimeout(handleQueue, 1000) + }) +} + +async function handleInteractionQueueing( + message: DiscordGatewayPayload, + shardId: number, +) { + if (message.t !== 'INTERACTION_CREATE') return + + const interaction = message.d as DiscordInteraction + // IF THIS INTERACTION IS NOT DEFERABLE + if ( + [ + InteractionTypes.ModalSubmit, + InteractionTypes.ApplicationCommandAutocomplete, + ].includes(interaction.type) + ) { + return await rest.runMethod( + rest, + 'POST', + routes.INTERACTION_ID_TOKEN(BigInt(interaction.id), interaction.token), + { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: { + content: `The bot is having a temporary issue, please try again or contact us at https://discord.gg/${BOT_SERVER_INVITE_CODE}`, + }, + }, + ) + } + + await rest.runMethod( + rest, + 'POST', + routes.INTERACTION_ID_TOKEN(BigInt(interaction.id), interaction.token), + { + // MESSAGE COMPONENTS NEED SPECIAL DEFER + type: + InteractionTypes.MessageComponent === interaction.type + ? InteractionResponseTypes.DeferredUpdateMessage + : InteractionResponseTypes.DeferredChannelMessageWithSource, + }, + ) + + // ADD EVENT TO QUEUE + queue.events.push({ shardId, message }) +} + +export type WorkerMessage = + | WorkerIdentifyShard + | WorkerAllowIdentify + | WorkerShardPayload + | WorkerGetShardInfo + +export type WorkerIdentifyShard = { + type: 'IDENTIFY_SHARD' + shardId: number +} + +export type WorkerAllowIdentify = { + type: 'ALLOW_IDENTIFY' + shardId: number +} + +export type WorkerShardPayload = { + type: 'SHARD_PAYLOAD' + shardId: number + data: ShardSocketRequest +} + +export type WorkerGetShardInfo = { + type: 'GET_SHARD_INFO' + nonce: string +} + +export type WorkerCreateData = { + intents: number + token: string + handlerUrls: string[] + handlerAuthorization: string + path: string + totalShards: number + workerId: number +} + +export type WorkerShardInfo = { + workerId: number + shardId: number + rtt: number + state: ShardState +} + +export interface QueuedEvent { + message: DiscordGatewayPayload + shardId: number +} + +export interface GatewayQueue { + processing: boolean + events: QueuedEvent[] +} +``` + +Note, you can take this concept and expand on it as much as you like. You can swap out the fetch() with websockets or +any other system you like to communicate between your processes. I highly recommend you take some time to add checks in +place to prevent adding to queue when the queue reaches a certain size. You don't want this to become a memory leak of +infinite size and crash your gateway. So take the time to do this right in your setup. + +If you have any questions, please contact us on our [discord server](https://discord.gg/ddeno). diff --git a/website/tutorial/big-bot-guide/rest.md b/website/tutorial/big-bot-guide/rest.md new file mode 100644 index 000000000..ce419d2e9 --- /dev/null +++ b/website/tutorial/big-bot-guide/rest.md @@ -0,0 +1,134 @@ +--- +sidebar_position: 2 +sidebar_label: Step 1 - REST +--- + +# Creating A Standalone REST Process + +The first thing we want to make is our standalone REST process. This process will be used by almost every other process, +so it is going to be the foundation of the bot. + +Before, we dive into how, here is a quick summary of why you will want a standalone REST process. + +## Why Use Standalone REST Process? + +- Easily host on any serverless infrastructure. +- Freedom from global rate limit errors + - As your bot grows, you want to handle global rate limits better. Shards don't communicate fast enough to truly + handle it properly so this allows 1 rest handler across the entire bot. + - In fact, you can host multiple instances of your bot and all connect to the same rest server. +- REST does not rest! + - Separate rest means if your bot for whatever reason crashes, your requests that are queued will still keep going and + will not be lost. + - Seamless updates! When you want to update and reboot the bot, you could potentially lose tons of messages or + responses that are in queue. Using this you could restart your bot without ever worrying about losing any responses. +- Single source of contact to Discord API + - This will allow you to make requests to discord from anywhere including a bot dashboard. You no longer need to have + to communicate to your bot processes just to make a request or anything. Free up your bot process for processing bot + events. +- Scalability! Scalability! Scalability! + +## Preparations + +Before going further, you should have already made the following pieces: + +- rest/mod.ts +- deps.ts (Make sure to import discordeno) +- configs.ts +- Deno extension(if you are using deno, this is required) +- TabNine extension to make your life so much better. (Optional) + +## Creating Rest Manager + +Now let's open up that rest file and start coding. + +```ts +import { DISCORD_TOKEN, REST_AUTHORIZATION, REST_PORT } from '../../configs.ts' +import { BASE_URL, createRestManager } from '../../deps.ts' + +const rest = createRestManager({ + token: DISCORD_TOKEN, + secretKey: REST_AUTHORIZATION, + customUrl: `http://localhost:${REST_PORT}`, +}) +``` + +- `createRestManager` is imported from your deps file which should have exported everything from discordeno. +- `DISCORD_TOKEN` is the bot's token itself. +- `REST_AUTHORIZATION` is a special password you want to use to authenticate that requests being sent to your port are + indeed from you. +- `customUrl` the url where this rest process will be running. This can be localhost which we are using in this guide if + you want all processes on same VPS or separate them to different servers for horizontal scaling. `REST_PORT` is just + the port where you want the process hosted. + +Now you have an entire Rest manager ready and waiting. Only thing you need now, is to listen for requests. + +## Creating HTTP Listener + +Since this is not a beginner guide, I am assuming you know already how to create a HTTP listener. There are enough +guides on this out there. I will only cover the rough functionality. + +```ts +// START LISTENING TO THE URL(localhost) +const server = Deno.listen({ port: REST_PORT }) +console.info( + `HTTP webserver running. Access it at: http://localhost:${REST_PORT}/`, +) + +// Connections to the server will be yielded up as an async iterable. +for await (const conn of server) { + // In order to not be blocking, we need to handle each connection individually + // in its own async function. + handleRequest(conn) +} + +async function handleRequest(conn: Deno.Conn) { + // This "upgrades" a network connection into an HTTP connection. + const httpConn = Deno.serveHttp(conn) + // Each request sent over the HTTP connection will be yielded as an async + // iterator from the HTTP connection. + for await (const requestEvent of httpConn) { + if ( + !REST_AUTHORIZATION || + REST_AUTHORIZATION !== requestEvent.request.headers.get('AUTHORIZATION') + ) { + return requestEvent.respondWith( + new Response(JSON.stringify({ error: 'Invalid authorization key.' }), { + status: 401, + }), + ) + } + + const json = (await requestEvent.request.json()) as any + + // IMPLEMENT ANY ERROR HANDLING HERE IF YOU WOULD LIKE BY WRAPPING THIS IN A CATCH + + // MAKE THE REQUEST TO DISCORD + const result = await rest.runMethod( + rest, + // USE THE SAME METHOD THAT CAME IN. IF DELETE CAME IN WE SEND DELETE OUT + requestEvent.request.method as any, + // OVERWRITE THE CUSTOM URL WITH DISCORDS BASE URL + `${BASE_URL}/v${rest.version}${requestEvent.request.url.substring( + rest.customUrl.length, + )}`, + json, + ) + + // RETURN DISCORDS RESPONSE BACK TO THE PROCESS MAKING THE REQUEST + if (result) { + requestEvent.respondWith( + new Response(JSON.stringify(result), { + status: 200, + }), + ) + } else { + requestEvent.respondWith( + new Response(undefined, { + status: 204, + }), + ) + } + } +} +``` diff --git a/website/tutorial/big-bot-guide/step-by-step.md b/website/tutorial/big-bot-guide/step-by-step.md new file mode 100644 index 000000000..4ec7f3ae3 --- /dev/null +++ b/website/tutorial/big-bot-guide/step-by-step.md @@ -0,0 +1,40 @@ +--- +sidebar_position: 1 +--- + +# Step By Step Guide + +THIS IS A WORK IN PROGRESS GUIDE USING THE NEW v16 OF DISCORDENO. + +## Understanding The Goals of This Guide + +This guide is a quick-paced walkthrough meant for big bot developers. It is expected that you have a decent amount of +understanding of how to code your bots. + +## Is This Guide Meant For You? + +If your goal is not to have a bot in millions of discord servers, please find another guide/library. Discordeno is +heavily opinionated towards optimizing for bots at scale. If you do not know what a Map or a Set is without having to +google it, you are at the wrong place. + +## Why You Should Use Discordeno? + +The best way I can describe why you should use Discordeno, is from the words of the biggest bot developers themselves. +After speaking to some of the developers of the biggest JS/TS bots, you begin to see a pattern of users unhappy with the +current state of JS/TS libraries. They are no longer able to help them scale easily and are starting to move away to +other libraries or having to make their own libraries because they need to be able to make their bot distributed. + +The following quotes are from developers who have bot's in atleast 1 million+ discord servers. + +- Flexibility like no other library. + - One of the big bot developers found that when their bot got too big, Eris was just very painful to optimize. + - "A pretty large hassle, I had to fork eris and modify it. There was a lot of interdependency on the values from + caches that made it difficult to remove properties "safely" without searching the entire codebase" + - When discovering how easy it was to do the same thing in Discordeno: + - "the convenience of being able to do so puts confidence in me that the lib is versatile so it'd certainly draw me + towards it" +- Scalability: Standalone Gateway, Rest, Event Handler, Commands, Cache and much more. + - "All this sound like a dream (especially when you currently use eris)" + +Discordeno provides you all the tools that you need to make bot development really easy. As the old saying goes, the +best way to learn to ride a bicycle is to actually try riding a bicycle. So let's try out Discordeno. diff --git a/website/tutorial/nodejs/CommandHandler/_category_.json b/website/tutorial/nodejs/CommandHandler/_category_.json new file mode 100644 index 000000000..dd4267393 --- /dev/null +++ b/website/tutorial/nodejs/CommandHandler/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Command Handler", + "position": 9 +} diff --git a/website/tutorial/nodejs/CommandHandler/command-manager.md b/website/tutorial/nodejs/CommandHandler/command-manager.md new file mode 100644 index 000000000..354be6a83 --- /dev/null +++ b/website/tutorial/nodejs/CommandHandler/command-manager.md @@ -0,0 +1,92 @@ +--- +sidebar_position: 2 +--- + +# Command Manager + +Currently, you probably have something like this in your code: + +```js +const Discord = require('discordeno.js') +// Ideally you should move to an `.env` file +const config = require('./config.json') + +const bot = Discord.createBot({ + events: { + messageCreate(client, message) { + if (message.content === '!ping') { + client.helpers.sendMessage(message.channelId, { content: 'pong' }) + } + }, + }, + intents: Discord.Intents.Guilds | Discord.Intents.GuildMessages, + token: config.token, +}) +const client = Discord.enableCachePlugin(bot, {}) + +Discord.startBot(client) +``` + +Of course, if you add more and more commands and as your code base grows, you can lose track very quickly. + +To avoid this, it is recommended to store the commands in separate folders divided into different categories. + +[Previously, we introduced you to our plugin structure, which has a lot of advantages.](../design.md) + +```root +├Plugins/ +├── General/ +│ ├── commands/ +│ │ ├── ping.js +│ │ └── ... +├── Developer/ +│ ├── commands/ +│ │ ├── eval.js +│ │ └── ... +└── ... +``` + +**Get [this file](https://github.com/discordeno/discordeno/tree/main/template/nodejs/Managers/CommandManager.js) from +the [nodejs template](https://github.com/discordeno/discordeno/tree/main/template)** + +```js +const CommandManager = require('./template/Managers/CommandManager.js') +const manager = new CommandManager({}) +manager.load({ plugin: true }) // Load the commands +client.commands = manager + +client.commands.cache.get('ping') // Get the `ping` command +``` + +The Manager will automatically iterate through all files in the folder and then load them into the cache property, which +is mapped on the command name. + +**Take a look at [Create Command](./create-command.md) to learn how to create a command.** + +## Handle Command + +The manager also contains a handler for executing the command when a message is received. + +:::important + +Currently checks for permissions, cooldowns, and rate limits are not covered, but these will be added soon. + +::: + +### Message Create Event: + +```js +module.exports = async (client, message) => { + client.commands.isCommand(message) +} +``` + +### Interaction Create Event: + +```js +module.exports = async (client, interaction) => { + client.commands.isInteraction(interaction) +} +``` + +You can also customize the `isCommand` function to your use case. diff --git a/website/tutorial/nodejs/CommandHandler/create-command.md b/website/tutorial/nodejs/CommandHandler/create-command.md new file mode 100644 index 000000000..806d71c61 --- /dev/null +++ b/website/tutorial/nodejs/CommandHandler/create-command.md @@ -0,0 +1,61 @@ +--- +sidebar_position: 3 +--- + +# Create Command + +One of the most important features we wanted in our template, was that you can use the same code for handling +`slash commands` and `message based commands`. + +This can be done by saving the static class in the command cache, creating a constructor and passing the desired data. +Moreover the `BaseCommand` is extended with the `Response Command` class, so you can take advantage of functions such as +`.reply()` + +**Copy the [`BaseCommand`](https://github.com/discordeno/discordeno/tree/main/template/nodejs/Structures/BaseCommand.js) +& +[`CommandResponses`](https://github.com/discordeno/discordeno/tree/main/template/nodejs/Structures/CommandResponses.js) +code from the template** + +### Creating a Ping Command: + +```js +const BaseCommand = require("../../../Structures/BaseCommand.js"); +const Embed = require("../../../Structures/Embed.js"); + +class pingCommand extends BaseCommand { + static name = "ping"; + static description = "See if the bot latency is okay"; + static usage = ""; + static category = "General"; + static slash = { name: "ping", category: "info" }; + + constructor(data) { + super(data); + } + + async execute() { + const msg = await this.reply({content: `Pinging...`}); + // Assign properties to the response + const ping = msg.timestamp - this.message.timestamp; + + const embed = new Embed() + .setTitle(`The Bots ping is ${ping} ms`) + .toJSON(); + + // Edit Message with the Embed + return await msg.edit({embeds: [embed] }); + }); + } +} + +module.exports = pingCommand; +``` + +- The `BaseCommand` is extended with the `CommandResponses` class. +- The ping command class is extended with the `BaseCommand` class. +- Some static properties are assigned to the ping command class, in order to access it in the cache, such as `name`, + `description`, `usage`, `category` and `slash`... +- The `execute()` function will be called, when the command has been run by the user. +- The constructor allows to access data, such as `this.message`, `this.args`, `this.client`... + +You can customize the `CommandManager` file, in order to pass arguments in the `execute()` function. diff --git a/website/tutorial/nodejs/CommandHandler/getting-started.md b/website/tutorial/nodejs/CommandHandler/getting-started.md new file mode 100644 index 000000000..622175240 --- /dev/null +++ b/website/tutorial/nodejs/CommandHandler/getting-started.md @@ -0,0 +1,22 @@ +--- +sidebar_position: 1 +--- + +# Getting Started with the Command Manager + +One of the most important characteristics of bots is that they have commands that can be used to interact with the bot. + +Hard coding your commands in an event function is not the best code practice and should be strictly prevented. + +In the following we will show you how to create a command manager, which is compatible with Discordeno's Client. + +- Load Commands +- Handle Commands +- Reload Commands + +:::info template + +You can also copy the +[`CommandManager` from the template repo](https://github.com/discordeno/discordeno/tree/main/template/nodejs/Managers/CommandManager.js). + +::: diff --git a/website/tutorial/nodejs/EventHandler/_category_.json b/website/tutorial/nodejs/EventHandler/_category_.json new file mode 100644 index 000000000..8dc1b47c3 --- /dev/null +++ b/website/tutorial/nodejs/EventHandler/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Event Handler", + "position": 8 +} diff --git a/website/tutorial/nodejs/EventHandler/event-manager.md b/website/tutorial/nodejs/EventHandler/event-manager.md new file mode 100644 index 000000000..9acf2eb79 --- /dev/null +++ b/website/tutorial/nodejs/EventHandler/event-manager.md @@ -0,0 +1,123 @@ +--- +sidebar_position: 2 +--- + +# Create Event Manager + +In order to process certain events, you must provide the Discordeno client with functions for these events. + +```js +const Discord = require('discordeno') +const config = require('./config.json') + +const client = Discord.createBot({ + events: { + ready(client, payload) { + console.log( + `Successfully connected Shard ${payload.shardId} to the gateway`, + ) + }, + + async messageCreate(client, message) { + if (message.content === '!ping') { + await client.helpers.sendMessage(message.channelId, { content: 'pong' }) + } + + console.log(`Received message: ${message.content || message.embeds}`) + }, + }, + intents: ['Guilds', 'GuildMessages'], + token: config.token, +}) + +Discord.startBot(client) +``` + +As you listen to more and more events, the functions code grows along with them, so you can quickly lose track. + +To avoid this, we recommend storing the event functions divided into files in a separate folder. + +## Create Event Folder + +Create a folder called `events` in your project folder. + +:::info note + +The event files have to be named using camelCase so that they can be understood by the client. e.g `message` -> +`messageCreate.js`. You can check the typings see how the events are called. + +::: + +Ready Event: + +```js +module.exports = (client, payload) => { + if (payload.shardId + 1 === client.gateway.maxShards) { + // All Shards are ready + console.log( + `Successfully connected to the gateway as ${payload.user.username}#${payload.user.discriminator}`, + ) + } +} +``` + +## Load your Events + +```js +const fs = require('fs') +const path = require('path') + +const resolveFolder = folderName => path.resolve(__dirname, '.', folderName) + +class EventManager { + constructor(client) { + this.cache = new Map() + this._events = {} + } + + load(options = {}) { + const eventsFolder = resolveFolder('../events') + fs.readdirSync(eventsFolder).map(async file => { + if (!file.endsWith('.js')) return + + const fileName = path.join(eventsFolder, file) + const event = require(fileName) + const eventName = file.split('.')[0] + + this._events[`${eventName}`] = event + }) + + return this._events + } +} + +module.exports = EventManager +``` + +The code above, which can also be found in the +[template repo](https://github.com/discordeno/discordeno/tree/main/template/nodejs/Managers/EventManager.js) will loop +through all the files in the `events` folder and load the functions into the `_events` object. + +In order to let the client know which events should be processed, you need to pass the functions in the +`createBot.events` object. + +```js +const Discord = require('discordeno') +const config = require('./config.json') + +const EventManager = require('./Managers/EventManager.js') +const events = new EventManager({}) + +const client = Discord.createBot({ + events: events.load({}), + intents: ['Guilds', 'GuildMessages'], + token: config.token, +}) + +Discord.startBot(client) +``` + +Moreover, you can customize the `EventManager` and add more functionality to it and make it exactly fit your needs or +even emit events, by extending it. + +Of course you wonder what you can do with all of this now. We will explain this further on the next page. diff --git a/website/tutorial/nodejs/EventHandler/getting-started.md b/website/tutorial/nodejs/EventHandler/getting-started.md new file mode 100644 index 000000000..350c2e655 --- /dev/null +++ b/website/tutorial/nodejs/EventHandler/getting-started.md @@ -0,0 +1,29 @@ +--- +sidebar_position: 1 +--- + +# Getting Started with the Event Handler + +An event handler is essential to process the data, which Discord sends to you. + +With a good implementation, you will have a nice code structure and thus have a good overview in long term. + +Since the `EventEmitter` class is commonly used you probably already know it from other libraries. + +Discordeno decided against it as it comes with several downsides which are mentioned below. + +- It's easy to create memory leaks, when you add too many listeners or go carelessly with it. +- Many fragmented parts of event code complicate maintenance. +- ErrorHandling is difficult and debugging is harder when many listeners are open for the same events. + +Performance plays a more important role than handling, however this event management system can be easily implemented +since it only needs a few changes in your code. + +In the following we will show you, how to create an event manager, which is compatible with Discordeno's Client. + +:::info template + +You can also copy the +[`EventManager` from the template repo](https://github.com/discordeno/discordeno/tree/main/template/nodejs/Managers/EventManager.js). + +::: diff --git a/website/tutorial/nodejs/EventHandler/handle-event.md b/website/tutorial/nodejs/EventHandler/handle-event.md new file mode 100644 index 000000000..a09cd928d --- /dev/null +++ b/website/tutorial/nodejs/EventHandler/handle-event.md @@ -0,0 +1,76 @@ +--- +sidebar_position: 3 +--- + +# Handle Events + +When an event is fired, Discordeno sends two important things: the `client` instance and the `payload`. + +As mentioned in the `Structure` section, the `payload` object does not contain any functions as it's a plain json +object. + +In order to take use of our nice built structures, we need to transform the payload into a structure. + +:::info + +The Structures can be found [here](https://github.com/discordeno/discordeno/tree/main/template/nodejs/Structures) + +::: + +Sometimes it's important to listen to events, in order to get informed of changes and updating the cache based on it. + +### Message Event + +This file should be called `messageCreate.js`. + +```js +const Message = require('./structures/Message') + +module.exports = async (client, payload) => { + const message = client.messages.forge(payload) + + if (message.author.bot) return + if (message.content === '!ping') return await message.reply('pong') +} +``` + +### Interaction Event + +This file should be called `interactionCreate.js`. + +```js +const Interaction = require('./structures/Interaction') + +module.exports = async (client, payload) => { + const interaction = client.interactions.forge(payload) + + if (interaction.data.name === 'ping') + return await interaction.reply({ content: 'pong' }) +} +``` + +### Ready Event + +This file should be called `ready.js`. + +:::tip + +There is a small difference with the `ready` Event. The Event is fired `shard` wise, in other words it fires every time +a `shard` becomes ready. + +::: + +In order to fire the "real event" a small code snippet has to be added to the `ready` Event. + +```js +const User = require('../Structures/User') + +module.exports = async (client, payload) => { + client.user = client.users.forge(payload.user) + + if (payload.shardId + 1 === client.gateway.maxShards) { + // All Shards are ready + console.log(`Successfully connected to the gateway as ${client.user.tag}`) + } +} +``` diff --git a/website/tutorial/nodejs/Structures/_category_.json b/website/tutorial/nodejs/Structures/_category_.json new file mode 100644 index 000000000..b7a1b76c1 --- /dev/null +++ b/website/tutorial/nodejs/Structures/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Structures", + "position": 7 +} diff --git a/website/tutorial/nodejs/Structures/collectors.md b/website/tutorial/nodejs/Structures/collectors.md new file mode 100644 index 000000000..63aa53d27 --- /dev/null +++ b/website/tutorial/nodejs/Structures/collectors.md @@ -0,0 +1,56 @@ +--- +sidebar_position: 5 +--- + +# Create Collectors + +Some of your commands or features are sometimes based on user interactions. E.g. if a user presses a button and you want +to know whether it was pressed. This is actually done by listening to the `interactionCreate` event. + +But sometimes you need to access locale variables or don't want to "hardcode" the part. + +That's why it's sometimes recommended to create collectors. + +Collectors are listeners that listen to a specific event. In addition, you can provide a filter, so you only receive +certain interactions. + +## Use a Collector + +:::note Template The template code is used below. You must have the EventManager part to use the collector feature. ::: + +We have a pre-made class for collectors which you can find +[here](https://github.com/meister03/discordeno.js/blob/master/Util/Collectors.js). + +```js +const Discord = require('discordeno.js') +const filter = m => + m.data?.customId === 'warn_modal' && m.user.id === interaction.user.id +const listener = client.eventListener // When the eventListener property is named different +const collector = new Discord.Collector('interactionCreate', { + client: client, + timeout: 60000, + filter, + max: 20, + listener, +}) +collector.on('collect', m => { + const interaction = client.interactions.forge(m) + // Stop Collector + // collector.stop(); +}) + +// Fires on a timeout, when the collector has reached the max amount of interactions or when it has been closed +collector.on('end', collected => { + // Map of Collected Interactions + console.log(collected) +}) +``` + +As you can see, this opens up many possibilities. You can listen to any event and get the interaction you need. + +### Collector Options + +`filter`: Function, just fire the event if the filter returns true. `timeout`: Number, the time in milliseconds until +the collector times out. `max`: Number, the max amount of interactions the collector can collect. `listener`: Function, +the listener that will be fired when the collector collects an interaction. Just required when client property is named +differently. diff --git a/website/tutorial/nodejs/Structures/components.md b/website/tutorial/nodejs/Structures/components.md new file mode 100644 index 000000000..6f938cc86 --- /dev/null +++ b/website/tutorial/nodejs/Structures/components.md @@ -0,0 +1,229 @@ +--- +sidebar_position: 4 +--- + +# Create Components + +Since Discord has decided to make message content accessible only to privileged bots, components will play an +increasingly important role in the future. Discord has released some components already and many more will follow. Of +course, this opens up completely new possibilities. On the one hand, it improves the user experience and on the other +hand, the interactions can be easily handled by the developer. + +To take advantage of this, we'll go into more detail on how to use them. + +:::note Runtime Overhead + +Constructor classes are nice to use and make your code look better, but they incur a slight runtime overhead compared to +just using raw data because they still execute methods, which takes more time to process. + +::: + +We already have a Template for `Components`, which can be found +[here](https://github.com/meister03/discordeno.js/tree/master/Structures/Component.js). + +## Different Components: + +There are many different components, which you can quickly read about here: + +### Action Row (`type: 1`): + +This is a top level component, which contains a limited amount of other components. It can be described as container. + +An Action Row ... + +- can not include an action row +- can maximal have 5 Buttons +- can have 1 SelectMenu +- can have 1 Text Input (only available in modal responses) + +### Button (`type: 2`): + +Buttons are interactive components, are bound to a message and they sent an interaction payload, when a user clicks on +it. + +![Different Button Styles](https://i.imgur.com/jUE2Kp0.png) + +- Needs a customId, except the Link Button +- An Action Row can have maximal 5 Buttons + +There are different styles of buttons, which can be used: + +- `1` - PRIMARY - blurple - customId required +- `2` - DEFAULT - grey - customId required +- `3` - SUCCESS - green - customId required +- `4` - DANGER - red - customId required +- `5` - LINK - grey - url required + +### Select Menu (`type: 3`): + +Select Menus are a simple drop-down with selectable options. They accept a set of allowed selects, which sends an +interaction payload, when a user selects sth. from the menu. + +![Select Menu](https://i.imgur.com/42Hwiuw.png) + +- You can specify a range of allowed selects (`minValue` and `maxValue`) +- Every Select Item can have an `emoji` and has a `value`, in order to identify the selected item +- A default Select Item can be set +- An Action Row can have maximal 1 Select Menu + +### Text Input (`type: 4`): + +Text Inputs are interactive components, which can just be sent with a modal response. + +- You can specify a range of text length (`minLength` and `maxLength`) +- You can add a placeholder, a pre-filled value and specify whether the text input is required +- An Action Row can have maximal 1 Text Input + +## Send Components + +As mentioned above there are different types of components. This requires to define a type, so that Discord knows, which +component you want to use. + +```js +class ActionRow { + constructor(options = {}) { + this.type = 1 + } + + setComponents(...components) { + this.components = components + return this + } +} +``` + +```js +const button = new Button() +const button2 = new Button() +const actionRow = new ActionRow().setComponents(button, button2) +``` + +This code will obviously not work because it's a missing a lot required of data. The other reason is that we can't send +a class to Discord, we need sth. to transform it to a json object. + +We have a pre-made class for components which you can find +[here](https://github.com/meister03/discordeno.js/tree/master/Structures/Component.js). + +### Button + +```js +const Discord = require('discordeno.js') +const message = client.messages.forge(rawMessage) + +const button = new Discord.Component() + .setType('BUTTON') + .setStyle('LINK') + .setLabel('Click me!') + .setUrl('https://google.com') + .toJSON() + +// Button with raw types +const button2 = new Discord.Component() + .setType(2) + .setStyle(4) + .setLabel('DO NOT CLICK') + .setCustomId('12345') + .toJSON() + +const actionRow = new Discord.Component() + .setType('ACTION_ROW') + .setComponents(button, button2) + .toJSON() + +// Message to send +const messageOptions = { content: 'hello', components: [actionRow] } + +// await client.helpers.sendMessage(channelId, messageOptions); // Do it the raw way +message.channel.send(messageOptions) // Do it with the structure +``` + +As you can see, for simplicity you can use strings instead of numbers (types), which are hard to remember. + +### Select Menu + +```js +const Discord = require('discordeno.js') +const message = client.messages.forge(rawMessage) + +const selectMenu = new Discord.Component() + .setType('SELECT_MENU') + .setCustomId('12345') + .setOptions([ + { + label: 'Option 1', + value: '1', + description: `This is option 1`, + }, + { + label: 'Option 2', + value: '2', + description: `This is option 2`, + }, + { + label: 'Default Option', + value: '3', + description: `Default option...`, + default: true, + }, + ]) + .setPlaceholder('Select an option') + .toJSON() + +const actionRow = new Discord.Component() + .setType('ACTION_ROW') + .setComponents(selectMenu) + .toJSON() + +const messageOptions = { content: 'hello', components: [actionRow] } + +// await client.helpers.sendMessage(channelId, messageOptions); // Do it the raw way +message.channel.send(messageOptions) // Do it with the structure +``` + +### Text Input + +```js +const Discord = require('discordeno.js') +const interaction = client.messages.forge(rawInteraction) + +const textInput = new Component() + .setType('TEXT_INPUT') + .setStyle('SHORT') + .setCustomId('t1') + .setLabel('User ID') + .setPlaceholder('User ID') + .setRequired(true) + .setMaxLength(20) + .setMinLength(1) + .toJSON() + +const textInput2 = new Component() + .setType('TEXT_INPUT') + .setStyle('PARAGRAPH') + .setCustomId('t2') + .setLabel('Reason') + .setPlaceholder('Reason for Ban') + .setRequired(false) + .setMaxLength(300) + .toJSON() + +const actionRow = new Component() + .setType('ACTION_ROW') + .setComponents(textInput) + .toJSON() +const actionRow2 = new Component() + .setType('ACTION_ROW') + .setComponents(textInput2) + .toJSON() + +interaction.popupModal({ + customId: 'ban_modal', + title: 'Ban User', + components: [actionRow, actionRow2], +}) +``` + +### Receive Interactions + +When a user clicks a button or selects an option from a Select Menu, Discord sends an `interactionCreate` event, which +contains the information necessary to process it. diff --git a/website/tutorial/nodejs/Structures/create-structure.md b/website/tutorial/nodejs/Structures/create-structure.md new file mode 100644 index 000000000..7bbaa6c4d --- /dev/null +++ b/website/tutorial/nodejs/Structures/create-structure.md @@ -0,0 +1,100 @@ +--- +sidebar_position: 2 +--- + +# Create Structure + +Structures are often used to transform data and add methods to existing objects. To make it easier to work with them. + +Imagine you have a channel object to which you want to send a message. + +```js +const data = { + id: 806947972004839444n, + name: 'spam-and-bots', +} +``` + +The recommended way would be: + +```js +await client.helpers.sendMessage(data.id, { content: 'hello' }) +``` + +However, you probably want to use something shorter, such as the following: + +```js +class Channel { + constructor(client, data) { + this.client = client + this.id = data.id + this.name = data.name + } + + async send(options) { + return await this.client.helpers.sendMessage(this.id, options) + } +} +``` + +Now you can use the `.send()` method on the channel object without using such a long code: + +```js +const channel = new Channel(client, data) +await channel.send({ content: 'hello' }) +``` + +Moreover, you can modify the `.send()` method to better suit your use case e.g not send the message if the channel is +blacklisted. + +This naturally opens a lot of opportunities and makes coding a lot easier. Because you decide what you want to do with +the data, how the methods are named and how you want to process the request. + +## Using Template Structures: + +When you are migrating from another library and you want to utilize the djs-like wrapper, you'll likely choose to +continue using special structures. Therefore we have ready-made structures for the wrapper `Discordeno.js`. + +- [Guild](https://github.com/meister03/discordeno.js/tree/master/Structures/Guild.js) +- [Channel](https://github.com/meister03/discordeno.js/tree/master/Structures/Channel.js) +- [Role](https://github.com/meister03/discordeno.js/tree/master/Structures/Role.js) +- [Member](https://github.com/meister03/discordeno.js/tree/master/Structures/Member.js) +- [User](https://github.com/meister03/discordeno.js/tree/master/Structures/User.js) +- [Message](https://github.com/meister03/discordeno.js/tree/master/Structures/Message.js) +- [Interaction](https://github.com/meister03/discordeno.js/tree/master/Structures/Interaction.js) +- [Emoji](https://github.com/meister03/discordeno.js/tree/master/Structures/Emoji.js) +- [Webhook](https://github.com/meister03/discordeno.js/tree/master/Structures/Webhook.js) +- [Embed](https://github.com/meister03/discordeno.js/tree/master/Structures/Embed.js) +- [Component](https://github.com/meister03/discordeno.js/tree/master/Structures/Component.js) +- [Collection](https://github.com/meister03/discordeno.js/tree/master/Structures/Collection.js) + +We recommend that you check the wrappers [Readme](https://github.com/meister03/discordeno.js#discordclient) in order to +construct the client for following the Guide + +**Using the Structures:** + +```js +const Discord = require('discordeno.js') +const client = new Discord.Client(clientOptions, cacheOptions) //See the Readme above +Discord.startBot(client) +const guild = client.guilds.forge(guildData) +const channel = guild.channels.forge(channelData) +const role = guild.roles.forge(roleData) +const member = guild.members.forge(memberData) +const user = guild.users.forge(userData) +const message = guild.messages.forge(messageData) +const interaction = guild.interactions.forge(interactionData) +const emoji = guild.emojis.forge(emojiData) + +const webhook = new Discord.Webhook(client, webhookData) +const embed = new Discord.Embed(embedData) // embedData is optional +const component = new Discord.Component(componentData) // componentData is optional +const collection = new Discord.Collection() +``` + +Some popular methods have been added to the structures so that you can use them without having to come up with your own. +In order to use the Structures from the Wrapper, you need to invoke the `.forge` method with the raw discord data, +whereas it will construct the structure for you. + +Next we're going to give a better insight into how create [`Embeds`](embeds) and [`Components`](components) with the +wrappers structures. diff --git a/website/tutorial/nodejs/Structures/embeds.md b/website/tutorial/nodejs/Structures/embeds.md new file mode 100644 index 000000000..fa40b8b39 --- /dev/null +++ b/website/tutorial/nodejs/Structures/embeds.md @@ -0,0 +1,112 @@ +--- +sidebar_position: 3 +--- + +# Create Embeds + +Embeds are widely used by bots in order to display messages in a fancy way. + +Unfortunately, the Discord API does not accept funky classes such as `new MessageEmbed().setTitle("hello")`, instead it +takes a json object, e.g. `{ title: "hello" }`. Therefore, we need to create an embed Structure that converts the +user-supplied data into the format which Discord uses. + +:::note Runtime Overhead + +Constructor classes are nice to use and make your code look better, but they incur a slight runtime overhead compared to +just using raw data because they still execute methods, which takes more time to process. + +::: + +```js +class Embed() { + constructor() {} + + setTitle(title) { + this.title = title; + } +} +``` + +Now we have created a class which we can use to create embeds. But we can't just send this to Discord. + +So we need an additional method which will convert the data from the class to the correct format. + +```js +class Embed(){ + constructor() {} + + setTitle(title) { + this.title = title; + } + + toJSON() { + return { + title: this.title + } + } +} +``` + +Wow, now you can create a embed and send it to Discord. + +```js +const Channel = require('./structures/Channel') // Path to structure + +const channel = new Channel(client, data) +await channel.send({ embeds: [embed] }) +``` + +You probably want more methods which you can use to create embeds. +[Here is how the Embed Structure looks like](https://github.com/meister03/discordeno.js/blob/master/Structures/Embed.js) + +### Using the Embed Structure: + +```js +const Discord = require('discordeno.js') + +const channel = client.channels.forge(channelData) +const showCaseEmbed = new Discord.Embed() + .setColor(0x00ae86) + .setTitle('A Random Title') + .setURL('https://github.com/discordeno') + .setAuthor({ + name: 'Author name', + iconUrl: + 'https://raw.githubusercontent.com/discordeno/discordeno/main/site/static/img/logo.png', + url: 'https://github.com/discordeno', + }) + .setDescription('A Random Description') + .setThumbnail( + 'https://raw.githubusercontent.com/discordeno/discordeno/main/site/static/img/logo.png', + ) + .addFields( + { name: 'Field 1 Name', value: 'Normal Field Value' }, + { name: '\u200B', value: '\u200B' }, + { name: 'Field 2 Name', value: 'Inline Field Value', inline: true }, + { name: 'Field 3 Name', value: 'Inline Field Value', inline: true }, + ) + .addField({ name: 'Field 4', value: 'Field Value' }) + .setImage( + 'https://raw.githubusercontent.com/discordeno/discordeno/main/site/static/img/logo.png', + ) + .setTimestamp() + .setFooter({ + text: 'A Footer Text', + iconUrl: + 'https://raw.githubusercontent.com/discordeno/discordeno/main/site/static/img/logo.png', + }) + .toJSON() + +await channel.send({ embeds: [showCaseEmbed] }) +``` + +### Embed Limits: + +- Title: 256 characters +- Description: 4096 characters +- Field Name: 256 characters +- Field Value: 1024 characters +- Footer Text: 2048 characters +- Author Name: 256 characters +- 10 Embeds per message +- In total over all 10 Embeds not more than 6000 characters diff --git a/website/tutorial/nodejs/Structures/getting-started.md b/website/tutorial/nodejs/Structures/getting-started.md new file mode 100644 index 000000000..b06806d6d --- /dev/null +++ b/website/tutorial/nodejs/Structures/getting-started.md @@ -0,0 +1,30 @@ +--- +sidebar_position: 1 +--- + +# Getting Started with Structures + +As previously mentioned, Discordeno was built with as few classes as possible, this is in favor of performance. + +For example, you cannot execute functions on objects. + +```diff +- message.channel.send({content: "hello"}) ++ client.helpers.sendMessage(message.channel.id, {content: "hello"}) +``` + +This seems to be more complicated at first, but has many advantages: + +- You get full control over the actions +- Errors are easier to debug +- A validation by classes does not have to take place + +One of the disadvantages is that you have to change a lot in your code. + +Of course, we recommend that you try out the upper way, but we will introduce structures in this guide because they are +used by many users who eventually want to migrate. + +For example, if you want to get correctly formatted objects, structures are obviously beneficial, because they support +the readability of the code by their ease of use + +In the following, we will introduce how to create your own structures and how to use the ones available in the template. diff --git a/website/tutorial/nodejs/_category_.json b/website/tutorial/nodejs/_category_.json new file mode 100644 index 000000000..b45c7e9ac --- /dev/null +++ b/website/tutorial/nodejs/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Nodejs", + "position": 3 +} diff --git a/website/tutorial/nodejs/create-application.md b/website/tutorial/nodejs/create-application.md new file mode 100644 index 000000000..b7197a8ba --- /dev/null +++ b/website/tutorial/nodejs/create-application.md @@ -0,0 +1,29 @@ +--- +sidebar_position: 3 +--- + +# Create Application + +1. Go to the [Developer Portal](https://discord.com/developers/applications) and create a new application. +2. Navigate to the Section `Bot` and confirm with "Yes, do it!" +3. Now copy your token and save it under a safe environment. + +:::caution Token Security + +Keep your token safe, because it is like a password that grants access to your bot, which then can be used for mass +DMing, mass banning or any other kind of malicious activity. + +::: + +## Add your Bot to your Server + +In order to use your Bot, it should be in a server where you can interact with it. + +1. Go to the [Developer Portal](https://discord.com/developers/applications) and click on your previously created bot. +2. Click on `OAuth2` and there go to the `URL Generator`. +3. Select the `bot` and the `applications.commands` scope. +4. Scroll down and select the `Administrator` permission. +5. Copy the generated URL and open it in your browser. +6. Select your Server and click the invite button. + +The bot should now have been added to your server and show as an offline user. diff --git a/website/tutorial/nodejs/design.md b/website/tutorial/nodejs/design.md new file mode 100644 index 000000000..86dacc19b --- /dev/null +++ b/website/tutorial/nodejs/design.md @@ -0,0 +1,203 @@ +--- +sidebar_position: 6 +--- + +# Design + +In order to ensure long-term scalability and maintainability, the code structure is of enormous importance. In the +following, we show how such a code structure could look like. + +The essential parts are a `CommandHandler/CommandManager`, `EventHandler/EventManager`, lots of `Structures` in order to +code faster and `Plugins`, where your different features will be, such as `Commands`, `DB Stuff`... + +## Code Structure + +We recommend following structure for your code: + +```root +├index.js +├─Structures/ +├─Managers/ +├─events/ +├─Plugins/ +├── General/ +│ ├── commands/ +│ │ ├── ping.js +│ │ └── ... +├── Developer/ +│ ├── commands/ +│ │ ├── eval.js +│ │ └── ... +├─Util/ +└── ... +``` + +The following explains why this structure is suitable. If you want to follow this guide further, you should create these +folders. + +In the `Managers` folder the Managers will be added e.g. `CommandManager.js`, `EventManager.js`. Generally codes, which +manage the system. + +While in the `Structures` folder mainly classes are added like `BaseCommand.js`, `CommandResponse.js`, `Embed.js`, +`Components.js`, which make it easier to add methods to objects. + +The `events` folder will contain the event handlers such as `messageCreate.js`, `debug.js` + +Your many useful features and categories end up in the `Plugins` folder, where they should be categorically divided into +many folders. + +The `Util` folder contains functions or classes that help you convert certain things, such as timestamps, into a +human-readable format. + +## CommandHandler & BaseCommand + +The `CommandHandler` is the main class of the bot, which will handle all the commands and the events received from +Discord. + +The `BaseCommand` is the base class of all commands, which will be extended with the`CommandResponse` class. + +### Steps showed in the following Guide + +- Loading commands from different plugins +- Deploying slash commands +- Handling `messageCreate` & `interactionCreate` events +- Command rate limit handling +- Handle `Interaction` & `Message` commands with the same code +- Validating user provided arguments +- Correct permission and error handling +- Hot reloading commands +- Creating message and interaction collectors + +## EventHandler + +You probably realized that Discordeno does not use an `EventEmitter` to fire the events, but your own event function is +fired. + +There are ways to adapt to an `EventEmitter`, but we decided against it for the following reasons: + +- It's easy to create memory leaks, when you add too many listeners or go carelessly with it. +- Many fragmented parts of event code complicate maintenance. +- ErrorHandling is difficult and debugging is harder when many listeners are open for the same events. + +## Structures + +Structures are essential to abstract larger parts of code in smaller ready-made methods and to modify them if necessary. + +Example: + +```js +class Command { + static name = 'ping' + static aliases = ['pong'] + static botPermission = ['SEND_EMBED_LINKS'] + + run(message, args) { + // do something + } +} +``` + +It would be annoying adding everytime the `botPermission` property to the class Command, when the Permission is used +from every Command, then it is unnecessary to add it, when you can extend the class. + +It would be annoying to add the `botPermission` property to the command class every time the same permissions are used +by each command. Extending the class makes this extra step obsolete. + +```js +class BaseCommand { + constructor(client) { + this.client = client + this.basePermission = ['SEND_EMBED_LINKS'] + } +} + +class Command extends BaseCommand { + static name = 'ping' + static aliases = ['pong'] + + constructor(data) { + super(data) + } + + run(message, args) { + // do something + } +} +``` + +## Plugins + +The plugins folder helps you categorize your code into many parts to give some structure. + +Of course, this has many advantages, you have a much clearer code, you can debug problems much easier. + +This also opens possibilities for open source contributions, since not all parts of the code have to be published in +order to add new plugins, since they are "independent". + +There will be the main `Plugins` folder, which by default contains a `General` folder for all your base commands. The +`Plugins` folder will also contain all your other plugins. + +## Error Handling + +One of the most important things is how to handle errors. This is done to provide a user-friendly experience and to find +errors faster. + +You should catch errors and log them in your logger so you can fix them later. There are several open source `Sentry`'s +that give you a good overview of the latest errors through a website. + +Sometimes errors have a positive effect on maintainability and scalability. + +In addition, handling errors caused by users is very important to increase transparency. If they don't know why the +error happened, then they'll be very surprised with what they did wrong and might even remove your bot from their +server. + +## Caching + +Normally libraries cache all the info they get, which can of course be helpful at the beginning to discover all +functionalities but later it turns out to be a resource-consuming method. Therefore, this way should be avoided. + +Discordeno allows `Custom Caching` and even `Custom Property Caching` which gives you fine-grained control over the +caching of data. Normally you only need 20% of the data received by Discord, which makes caching unnecessary in most +cases. + +There are also some `Filter` and `Sweeper` methods which help you to empty unused cache values. + +## Cross Communication & Scaling + +If you are running many different processes, such as a Welcomer API, communication is of central importance in order to +send or receive data, with which you can then perform certain actions. + +Cross communication can be easily done with sockets or a TCP client. + +This brings up this Structure: + +```js +Bridge (Heart) +- Machine 1 + - Cluster [0-9] +- Machine 2 + - Cluster [10-18] +- Machine 3 -> Welcomer Api +- Machine 4 -> Dashboard +``` + +It's important to use something fast to have a proper "real time" communication. + +Discordeno already offers many internal options for scaling bots, no matter what size. + +As you scale, you will likely separate many parts of your bot and put them in separate processes, such as a +`RestManager`, a `Gateway Manager` etc. + +This of course opens up a lot of possibilities: + +- Zero downtime updates +- Global cache +- Synced rate limits + +[Check the Github Readme for more information](https://github.com/discordeno/discordeno#features) + +:::tip congratulations + +You just learned how to design a scalable bot, let's get into implementing it with the next pages. + +::: diff --git a/website/tutorial/nodejs/getting-started.md b/website/tutorial/nodejs/getting-started.md new file mode 100644 index 000000000..04a65d5d3 --- /dev/null +++ b/website/tutorial/nodejs/getting-started.md @@ -0,0 +1,48 @@ +--- +sidebar_position: 1 +--- + +# Getting Started + +If you are reading this, you probably want to create a Discord bot with Discordeno or migrate from popular libraries like Discord.js. + +If this is going to be your first time making a bot, you should use Deno instead of Node.js. Although in some cases, Deno might not be suitable for you because of missing packages or a code base that is too large to migrate to a slightly different language. + +This guide will help you make your first Discord bot using Node.js or even migrate your bot from another library. + +Moreover this guide will utilize two different options. One option to use the Discordeno package without any frameworks +and one, which uses the wrapper called `Discordeno.js`, which aims to achieve a djs-like interface. + +:::important Disclaimer + +Some features are not documented yet. If you want to know more about them, kindly ask for help in the +[Discord Server](https://discord.gg/ddeno). + +::: + +## Why should I switch? + +Discordeno was built with the purpose of being scalable, flexible and easy to use. + +Libraries like `Discord.js` and `Eris` often have excessive caching behavior that can only be changed slightly without +breaking the entire library. There is a lack of customization and many nested classes, which makes it almost impossible +to edit the code without having unwanted side effects. Moreover scalability is only possible on a limited extend. + +Discordeno has been kept plain and simple, which opens up a lot of opportunities for customization such as +`custom-caching (custom-property-caching)`, [`Standalone Rest`](../big-bot-guide/rest.md), +[`Gateway`](../big-bot-guide/gateway.md), [`Cache`](../big-bot-guide/cache.md) and more. Check the detailed advantages +[here](https://github.com/discordeno/discordeno#features). + +This guide will also help you making your code more scalable and easier to maintain with bringing you closer to the +Discord API. + +# Before you start + +Before you start digging in this guide, you should have a solid understanding of `javascript`. If you are not familiar +with it, then you should take a look at some popular resources. + +- [W3Schools Course](https://www.w3schools.com/js/DEFAULT.asp) +- [Mozilla Docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript) +- [JavaScript.Info](https://javascript.info) + +A basic understanding is of great importance in order to solve problems skillfully. diff --git a/website/tutorial/nodejs/initial-setup.md b/website/tutorial/nodejs/initial-setup.md new file mode 100644 index 000000000..76449266f --- /dev/null +++ b/website/tutorial/nodejs/initial-setup.md @@ -0,0 +1,48 @@ +--- +sidebar_position: 4 +--- + +# Initial Setup + +## Config File + +Ideally, you should save your configs in an `.env` file. Out of simplicity for this guide, we are saving it in a +`config.json` file. + +Create a file named `config.json` in your project folder and insert the following content: + +```json +{ + "token": "YOUR_TOKEN_HERE", + "prefix": "!" +} +``` + +## Edit the main file + +Open the `index.js` file which you have created earlier and then insert the following content: + +```js +const Discord = require('discordeno') +const config = require('./config.json') + +const client = Discord.createBot({ + events: { + ready(client, payload) { + console.log( + `Successfully connected Shard ${payload.shardId} to the gateway`, + ) + }, + }, + intents: ['Guilds', 'GuildMessages'], + token: config.token, +}) + +Discord.startBot(client) +``` + +Now you can start your bot by running the following command in your terminal: + +```cli +$ node index.js +``` diff --git a/website/tutorial/nodejs/installion.md b/website/tutorial/nodejs/installion.md new file mode 100644 index 000000000..210daf415 --- /dev/null +++ b/website/tutorial/nodejs/installion.md @@ -0,0 +1,30 @@ +--- +sidebar_position: 2 +--- + +# Installing Node.js and Discordeno + +To use the Discordeno library you first need to install Node.js and then Discordeno from NPM. + +Go to [nodejs.org](https://nodejs.org/en/) and download the latest version of Node.js. Open the downloaded file and +follow the instructions of the installer to install Node.js. + +## Create a Folder + +Open your file manager and create a new folder (e.g.: `discordbot`) in your desired directory. Then open the code editor +of your choice and create a new file (e.g.: `index.js`) in the folder you just have created. + +### Initalize NPM & Install Discordeno + +In order to keep track of the dependencies, you need to initialize NPM, which generates a `package.json` file. + +```cli +$ npm init --yes +``` + +Then you need to install Discordeno. When you want to go along with the wrapper named `Discordeno.js`, then install it +too. Go to your terminal and run the following command: + +```cli +$ npm install discordeno +``` diff --git a/website/tutorial/nodejs/slash-command.md b/website/tutorial/nodejs/slash-command.md new file mode 100644 index 000000000..989bf76fb --- /dev/null +++ b/website/tutorial/nodejs/slash-command.md @@ -0,0 +1,70 @@ +--- +sidebar_position: 5 +--- + +# Slash Commands + +Since Discord has decided to make message content accessible only to privileged bots, message commands will play a +subordinate role in the future. Discord users will be more used to slash commands. That's why it's essential that every +bot offers them. + +In the following we will show you how to create slash commands: + +## Deploying Slash Commands + +There is a difference between global and guild commands. Global commands take a while to appear in all guilds. Guild +commands show up directly. + +For this reason, we will now show how to create guild commands, in order to test them immediately. + +```js +const guildId = BigInt('YOUR_GUILD_ID') +const command = { + name: 'ping', + description: 'Retrieves the Bot latency', + options: [], +} + +client.helpers.createApplicationCommand(command, guildId) +``` + +This is just very simple example, you can also add sub commands, select options and much more. + +## Handling Slash Commands + +Discord sends a WebSocket Event when a user runs a slash command. You can listen to this event by adding the +`interactionCreate` function in the client. + +```js +const Discord = require('discordeno') +const config = require('./config.json') + +const client = Discord.createBot({ + events: { + ready(client, payload) { + console.log( + `Successfully connected Shard ${payload.shardId} to the gateway`, + ) + }, + async interactionCreate(client, interaction) { + if (interaction.data?.name === 'ping') { + return await client.helpers.sendInteractionResponse( + interaction.id, + interaction.token, + { + type: Discord.InteractionResponseTypes.ChannelMessageWithSource, + data: { content: '🏓 Pong!' }, + }, + ) + } + }, + }, + intents: Discord.Intents.Guilds | Discord.Intents.GuildMessages, + token: config.token, +}) + +Discord.startBot(client) +``` + +The handling may see complicated in the beginning, but as mentioned before, we will introduce structures to make it +easier. diff --git a/website/yarn.lock b/website/yarn.lock index 7571730ae..ea2191fe6 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -4239,6 +4239,102 @@ __metadata: languageName: node linkType: hard +"@reactflow/background@npm:11.3.6": + version: 11.3.6 + resolution: "@reactflow/background@npm:11.3.6" + dependencies: + "@reactflow/core": "npm:11.10.1" + classcat: "npm:^5.0.3" + zustand: "npm:^4.4.1" + peerDependencies: + react: ">=17" + react-dom: ">=17" + checksum: c08805c2335f46a1088c84d7f1d6f4d788870144a32f8bdbea285d37cea7e9305409b2e92027a1c14bba55136b8c578a988389613ce1dcb0861f8a334bcfe7e9 + languageName: node + linkType: hard + +"@reactflow/controls@npm:11.2.6": + version: 11.2.6 + resolution: "@reactflow/controls@npm:11.2.6" + dependencies: + "@reactflow/core": "npm:11.10.1" + classcat: "npm:^5.0.3" + zustand: "npm:^4.4.1" + peerDependencies: + react: ">=17" + react-dom: ">=17" + checksum: 031ee82e60b3736c7e3e795f8d5fcdcb0af9e70e2319fb8805c0226e479d900e6e657e8a977939804b66ff69aa24480efe2f3e6e268373ab34590155d1479608 + languageName: node + linkType: hard + +"@reactflow/core@npm:11.10.1": + version: 11.10.1 + resolution: "@reactflow/core@npm:11.10.1" + dependencies: + "@types/d3": "npm:^7.4.0" + "@types/d3-drag": "npm:^3.0.1" + "@types/d3-selection": "npm:^3.0.3" + "@types/d3-zoom": "npm:^3.0.1" + classcat: "npm:^5.0.3" + d3-drag: "npm:^3.0.0" + d3-selection: "npm:^3.0.0" + d3-zoom: "npm:^3.0.0" + zustand: "npm:^4.4.1" + peerDependencies: + react: ">=17" + react-dom: ">=17" + checksum: 75b27a08c03424ba81d43427a7ff43a93b7708d97ad6026e92429891e22ebad13e5ab8bbd8a4e4bff986ff472ed71722f8d8bb59b10d0d658b8a7c92f91574a7 + languageName: node + linkType: hard + +"@reactflow/minimap@npm:11.7.6": + version: 11.7.6 + resolution: "@reactflow/minimap@npm:11.7.6" + dependencies: + "@reactflow/core": "npm:11.10.1" + "@types/d3-selection": "npm:^3.0.3" + "@types/d3-zoom": "npm:^3.0.1" + classcat: "npm:^5.0.3" + d3-selection: "npm:^3.0.0" + d3-zoom: "npm:^3.0.0" + zustand: "npm:^4.4.1" + peerDependencies: + react: ">=17" + react-dom: ">=17" + checksum: 8a56c5fa25a5719f1e3b5b896eee2f222364eff4c9ab7305d9e23e5e22fe177244dddc66fe605a76089ce3dcba7df1415dc3b7f4f86edaf8afdf953ddda651c4 + languageName: node + linkType: hard + +"@reactflow/node-resizer@npm:2.2.6": + version: 2.2.6 + resolution: "@reactflow/node-resizer@npm:2.2.6" + dependencies: + "@reactflow/core": "npm:11.10.1" + classcat: "npm:^5.0.4" + d3-drag: "npm:^3.0.0" + d3-selection: "npm:^3.0.0" + zustand: "npm:^4.4.1" + peerDependencies: + react: ">=17" + react-dom: ">=17" + checksum: 2b20cfe669f6172d0b8270476830a3b1e5c89aa32ad77564624b912bd7c389c78d543a7ee7497550712f0ea6a3cc70d60ed484cfb599eaa98797c13e7b96d949 + languageName: node + linkType: hard + +"@reactflow/node-toolbar@npm:1.3.6": + version: 1.3.6 + resolution: "@reactflow/node-toolbar@npm:1.3.6" + dependencies: + "@reactflow/core": "npm:11.10.1" + classcat: "npm:^5.0.3" + zustand: "npm:^4.4.1" + peerDependencies: + react: ">=17" + react-dom: ">=17" + checksum: 31902bff3db5bf441a8127095fab0ba27a10c93e8ce3c59d8b23014336f53d16be25bde4476858516f4e3b86b6fb2456c68aaee81a10ea8f6f0dcd2e2bfa2f21 + languageName: node + linkType: hard + "@sideway/address@npm:^4.1.3": version: 4.1.4 resolution: "@sideway/address@npm:4.1.4" @@ -4538,6 +4634,278 @@ __metadata: languageName: node linkType: hard +"@types/d3-array@npm:*": + version: 3.2.1 + resolution: "@types/d3-array@npm:3.2.1" + checksum: 4a9ecacaa859cff79e10dcec0c79053f027a4749ce0a4badeaff7400d69a9c44eb8210b147916b6ff5309be049030e7d68a0e333294ff3fa11c44aa1af4ba458 + languageName: node + linkType: hard + +"@types/d3-axis@npm:*": + version: 3.0.6 + resolution: "@types/d3-axis@npm:3.0.6" + dependencies: + "@types/d3-selection": "npm:*" + checksum: 8af56b629a0597ac8ef5051b6ad5390818462d8e588e1b52fb181808b1c0525d12a658730fad757e1ae256d0db170a0e29076acdef21acc98b954608d1c37b84 + languageName: node + linkType: hard + +"@types/d3-brush@npm:*": + version: 3.0.6 + resolution: "@types/d3-brush@npm:3.0.6" + dependencies: + "@types/d3-selection": "npm:*" + checksum: 4095cee2512d965732147493c471a8dd97dfb5967479d9aef43397f8b0e074b03296302423b8379c4274f9249b52bd1d74cc021f98d4f64b5a8a4a7e6fe48335 + languageName: node + linkType: hard + +"@types/d3-chord@npm:*": + version: 3.0.6 + resolution: "@types/d3-chord@npm:3.0.6" + checksum: ca9ba8b00debd24a2b51527b9c3db63eafa5541c08dc721d1c52ca19960c5cec93a7b1acfc0ec072dbca31d134924299755e20a4d1d4ee04b961fc0de841b418 + languageName: node + linkType: hard + +"@types/d3-color@npm:*": + version: 3.1.3 + resolution: "@types/d3-color@npm:3.1.3" + checksum: 1cf0f512c09357b25d644ab01b54200be7c9b15c808333b0ccacf767fff36f17520b2fcde9dad45e1bd7ce84befad39b43da42b4fded57680fa2127006ca3ece + languageName: node + linkType: hard + +"@types/d3-contour@npm:*": + version: 3.0.6 + resolution: "@types/d3-contour@npm:3.0.6" + dependencies: + "@types/d3-array": "npm:*" + "@types/geojson": "npm:*" + checksum: e7b7e3972aa71003c21f2c864116ffb95a9175a62ec56ec656a855e5198a66a0830b2ad7fc26811214cfa8c98cdf4190d7d351913ca0913f799fbcf2a4c99b2d + languageName: node + linkType: hard + +"@types/d3-delaunay@npm:*": + version: 6.0.4 + resolution: "@types/d3-delaunay@npm:6.0.4" + checksum: cb8d2c9ed0b39ade3107b9792544a745b2de3811a6bd054813e9dc708b1132fbacd796e54c0602c11b3a14458d14487c5276c1affb7c2b9f25fe55fff88d6d25 + languageName: node + linkType: hard + +"@types/d3-dispatch@npm:*": + version: 3.0.6 + resolution: "@types/d3-dispatch@npm:3.0.6" + checksum: f82076c7d205885480d363c92c19b8e0d6b9e529a3a78ce772f96a7cc4cce01f7941141f148828337035fac9676b13e7440565530491d560fdf12e562cb56573 + languageName: node + linkType: hard + +"@types/d3-drag@npm:*, @types/d3-drag@npm:^3.0.1": + version: 3.0.7 + resolution: "@types/d3-drag@npm:3.0.7" + dependencies: + "@types/d3-selection": "npm:*" + checksum: 93aba299c3a8d41ee326c5304ab694ceea135ed115c3b2ccab727a5d9bfc935f7f36d3fc416c013010eb755ac536c52adfcb15c195f241dc61f62650cc95088e + languageName: node + linkType: hard + +"@types/d3-dsv@npm:*": + version: 3.0.7 + resolution: "@types/d3-dsv@npm:3.0.7" + checksum: 8507f542135cae472781dff1c3b391eceedad0f2032d24ac4a0814e72e2f6877e4ddcb66f44627069977ee61029dc0a729edf659ed73cbf1040f55a7451f05ef + languageName: node + linkType: hard + +"@types/d3-ease@npm:*": + version: 3.0.2 + resolution: "@types/d3-ease@npm:3.0.2" + checksum: d8f92a8a7a008da71f847a16227fdcb53a8938200ecdf8d831ab6b49aba91e8921769761d3bfa7e7191b28f62783bfd8b0937e66bae39d4dd7fb0b63b50d4a94 + languageName: node + linkType: hard + +"@types/d3-fetch@npm:*": + version: 3.0.7 + resolution: "@types/d3-fetch@npm:3.0.7" + dependencies: + "@types/d3-dsv": "npm:*" + checksum: d496475cec7750f75740936e750a0150ca45e924a4f4697ad2c564f3a8f6c4ebc1b1edf8e081936e896532516731dbbaf2efd4890d53274a8eae13f51f821557 + languageName: node + linkType: hard + +"@types/d3-force@npm:*": + version: 3.0.9 + resolution: "@types/d3-force@npm:3.0.9" + checksum: e7f2260b9d57d0623f24b2876a10f6e4f7b9690035a5d9e776513b5f9261d0d1a8c436b0b977445f1753686b88ce1cecf3dd4538907da833c6ddc03cfbf45936 + languageName: node + linkType: hard + +"@types/d3-format@npm:*": + version: 3.0.4 + resolution: "@types/d3-format@npm:3.0.4" + checksum: b937ecd2712d4aa38d5b4f5daab9cc8a576383868be1809e046aec99eeb1f1798c139f2e862dc400a82494c763be46087d154891773417f8eb53c73762ba3eb8 + languageName: node + linkType: hard + +"@types/d3-geo@npm:*": + version: 3.1.0 + resolution: "@types/d3-geo@npm:3.1.0" + dependencies: + "@types/geojson": "npm:*" + checksum: e759d98470fe605ff0088247af81c3197cefce72b16eafe8acae606216c3e0a9f908df4e7cd5005ecfe13b8ac8396a51aaa0d282f3ca7d1c3850313a13fac905 + languageName: node + linkType: hard + +"@types/d3-hierarchy@npm:*": + version: 3.1.6 + resolution: "@types/d3-hierarchy@npm:3.1.6" + checksum: 0a76d79f0082ec260fc42c96422fde9be9bd7be7a32dd692fe8239c53757bf02469e5e839d72f3ff05b77df80783328dbf312ae763b6120de2805355cc838e97 + languageName: node + linkType: hard + +"@types/d3-interpolate@npm:*": + version: 3.0.4 + resolution: "@types/d3-interpolate@npm:3.0.4" + dependencies: + "@types/d3-color": "npm:*" + checksum: 72a883afd52c91132598b02a8cdfced9e783c54ca7e4459f9e29d5f45d11fb33f2cabc844e42fd65ba6e28f2a931dcce1add8607d2f02ef6fb8ea5b83ae84127 + languageName: node + linkType: hard + +"@types/d3-path@npm:*": + version: 3.0.2 + resolution: "@types/d3-path@npm:3.0.2" + checksum: fc974ffd75ff9268ea689cec764fffdd582d96f6f113db5bd2aba6d5d4a843eccc7919730fbca9134bd97ba6c91c13018a0c7c3f0813b73259f3603d140e0cb5 + languageName: node + linkType: hard + +"@types/d3-polygon@npm:*": + version: 3.0.2 + resolution: "@types/d3-polygon@npm:3.0.2" + checksum: 7cf1eadb54f02dd3617512b558f4c0f3811f8a6a8c887d9886981c3cc251db28b68329b2b0707d9f517231a72060adbb08855227f89bef6ef30caedc0a67cab2 + languageName: node + linkType: hard + +"@types/d3-quadtree@npm:*": + version: 3.0.6 + resolution: "@types/d3-quadtree@npm:3.0.6" + checksum: 4c260c9857d496b7f112cf57680c411c1912cc72538a5846c401429e3ed89a097c66410cfd38b394bfb4733ec2cb47d345b4eb5e202cbfb8e78ab044b535be02 + languageName: node + linkType: hard + +"@types/d3-random@npm:*": + version: 3.0.3 + resolution: "@types/d3-random@npm:3.0.3" + checksum: 2c126dda6846f6c7e02c9123a30b4cdf27f3655d19b78456bbb330fbac27acceeeb987318055d3964dba8e6450377ff737db91d81f27c81ca6f4522c9b994ef2 + languageName: node + linkType: hard + +"@types/d3-scale-chromatic@npm:*": + version: 3.0.3 + resolution: "@types/d3-scale-chromatic@npm:3.0.3" + checksum: cc5488af1136c3f9e28aa3c3ee2dc3e5e843c666f64360fb3870f0b8679cd2ee844edaa5a93504a9665deb98cb3c2ae2257d610c338fa8caa4a31ab6fdeb2f15 + languageName: node + linkType: hard + +"@types/d3-scale@npm:*": + version: 4.0.8 + resolution: "@types/d3-scale@npm:4.0.8" + dependencies: + "@types/d3-time": "npm:*" + checksum: 376e4f2199ee6db70906651587a4521976920fa5eaa847a976c434e7a8171cbfeeab515cc510c5130b1f64fcf95b9750a7fd21dfc0a40fc3398641aa7dd4e7e2 + languageName: node + linkType: hard + +"@types/d3-selection@npm:*, @types/d3-selection@npm:^3.0.3": + version: 3.0.10 + resolution: "@types/d3-selection@npm:3.0.10" + checksum: a62227d59fdd2debda326ca58613af64f8a5819f68b94aecb6b990616f5889a6d3d12816521bcd0c5c44df5d81654c558ba49a4570a766c6460bf2a765bdb10b + languageName: node + linkType: hard + +"@types/d3-shape@npm:*": + version: 3.1.6 + resolution: "@types/d3-shape@npm:3.1.6" + dependencies: + "@types/d3-path": "npm:*" + checksum: 75abf403ec5b8c11e761256aa6b3546533d61e2e12f15c82bed6b606e963dcdfb9868a2038c46099173c8830423b35ddaf14d1162f96ad9da18a2e90b0fa7d25 + languageName: node + linkType: hard + +"@types/d3-time-format@npm:*": + version: 4.0.3 + resolution: "@types/d3-time-format@npm:4.0.3" + checksum: 9dfc1516502ac1c657d6024bdb88b6dc7e21dd7bff88f6187616cf9a0108250f63507a2004901ece4f97cc46602005a2ca2d05c6dbe53e8a0f6899bd60d4ff7a + languageName: node + linkType: hard + +"@types/d3-time@npm:*": + version: 3.0.3 + resolution: "@types/d3-time@npm:3.0.3" + checksum: 4e6bf24ec422f0893747e5020592e107bb3d96764a43d5f0bff666202bd71f052c73f735b50ec66296a6efd5766ca40b6a4e8ce3bbc61217dbe9467340608c12 + languageName: node + linkType: hard + +"@types/d3-timer@npm:*": + version: 3.0.2 + resolution: "@types/d3-timer@npm:3.0.2" + checksum: 1643eebfa5f4ae3eb00b556bbc509444d88078208ec2589ddd8e4a24f230dd4cf2301e9365947e70b1bee33f63aaefab84cd907822aae812b9bc4871b98ab0e1 + languageName: node + linkType: hard + +"@types/d3-transition@npm:*": + version: 3.0.8 + resolution: "@types/d3-transition@npm:3.0.8" + dependencies: + "@types/d3-selection": "npm:*" + checksum: 6b4d4062486857036263de499f3cde861935e11784e9f7b834254625d87655376e03eec61bca204fd5fde29434722d1633bb12c8654bcf62632fe126b3ac90ce + languageName: node + linkType: hard + +"@types/d3-zoom@npm:*, @types/d3-zoom@npm:^3.0.1": + version: 3.0.8 + resolution: "@types/d3-zoom@npm:3.0.8" + dependencies: + "@types/d3-interpolate": "npm:*" + "@types/d3-selection": "npm:*" + checksum: cc6ba975cf4f55f94933413954d81b87feb1ee8b8cee8f2202cf526f218dcb3ba240cbeb04ed80522416201c4a7394b37de3eb695d840a36d190dfb2d3e62cb5 + languageName: node + linkType: hard + +"@types/d3@npm:^7.4.0": + version: 7.4.3 + resolution: "@types/d3@npm:7.4.3" + dependencies: + "@types/d3-array": "npm:*" + "@types/d3-axis": "npm:*" + "@types/d3-brush": "npm:*" + "@types/d3-chord": "npm:*" + "@types/d3-color": "npm:*" + "@types/d3-contour": "npm:*" + "@types/d3-delaunay": "npm:*" + "@types/d3-dispatch": "npm:*" + "@types/d3-drag": "npm:*" + "@types/d3-dsv": "npm:*" + "@types/d3-ease": "npm:*" + "@types/d3-fetch": "npm:*" + "@types/d3-force": "npm:*" + "@types/d3-format": "npm:*" + "@types/d3-geo": "npm:*" + "@types/d3-hierarchy": "npm:*" + "@types/d3-interpolate": "npm:*" + "@types/d3-path": "npm:*" + "@types/d3-polygon": "npm:*" + "@types/d3-quadtree": "npm:*" + "@types/d3-random": "npm:*" + "@types/d3-scale": "npm:*" + "@types/d3-scale-chromatic": "npm:*" + "@types/d3-selection": "npm:*" + "@types/d3-shape": "npm:*" + "@types/d3-time": "npm:*" + "@types/d3-time-format": "npm:*" + "@types/d3-timer": "npm:*" + "@types/d3-transition": "npm:*" + "@types/d3-zoom": "npm:*" + checksum: 12234aa093c8661546168becdd8956e892b276f525d96f65a7b32fed886fc6a569fe5a1171bff26fef2a5663960635f460c9504a6f2d242ba281a2b6c8c6465c + languageName: node + linkType: hard + "@types/debug@npm:^4.0.0": version: 4.1.12 resolution: "@types/debug@npm:4.1.12" @@ -4613,6 +4981,13 @@ __metadata: languageName: node linkType: hard +"@types/geojson@npm:*": + version: 7946.0.13 + resolution: "@types/geojson@npm:7946.0.13" + checksum: b3b68457c89bc3f0445dc9eb54d07e6f89658672867c54989bc7f71f87d54e562195b291d43e1b84476493351271d7ccb9f5c6ab2012b29fbafbb0e8e43c4bca + languageName: node + linkType: hard + "@types/gtag.js@npm:^0.0.12": version: 0.0.12 resolution: "@types/gtag.js@npm:0.0.12" @@ -6315,6 +6690,13 @@ __metadata: languageName: node linkType: hard +"classcat@npm:^5.0.3, classcat@npm:^5.0.4": + version: 5.0.4 + resolution: "classcat@npm:5.0.4" + checksum: 77373c58fa15ad2d4494b5c73c7ed2f859e7126227c357a3931e3f2a28e45dd9d8e779c1c8d3a8ba9ece833b21f14cd79160a7999973e28888d7e47f56c83170 + languageName: node + linkType: hard + "clean-css@npm:^5.2.2": version: 5.3.2 resolution: "clean-css@npm:5.3.2" @@ -6973,6 +7355,88 @@ __metadata: languageName: node linkType: hard +"d3-color@npm:1 - 3": + version: 3.1.0 + resolution: "d3-color@npm:3.1.0" + checksum: 536ba05bfd9f4fcd6fa289b5974f5c846b21d186875684637e22bf6855e6aba93e24a2eb3712985c6af3f502fbbfa03708edb72f58142f626241a8a17258e545 + languageName: node + linkType: hard + +"d3-dispatch@npm:1 - 3": + version: 3.0.1 + resolution: "d3-dispatch@npm:3.0.1" + checksum: 2b82f41bf4ef88c2f9033dfe32815b67e2ef1c5754a74137a74c7d44d6f0d6ecfa934ac56ed8afe358f6c1f06462e8aa42ca0a388397b5b77a42721570e80487 + languageName: node + linkType: hard + +"d3-drag@npm:2 - 3, d3-drag@npm:^3.0.0": + version: 3.0.0 + resolution: "d3-drag@npm:3.0.0" + dependencies: + d3-dispatch: "npm:1 - 3" + d3-selection: "npm:3" + checksum: 80bc689935e5a46ee92b2d7f71e1c792279382affed9fbcf46034bff3ff7d3f50cf61a874da4bdf331037292b9e7dca5c6401a605d4bb699fdcb4e0c87e176ec + languageName: node + linkType: hard + +"d3-ease@npm:1 - 3": + version: 3.0.1 + resolution: "d3-ease@npm:3.0.1" + checksum: 985d46e868494e9e6806fedd20bad712a50dcf98f357bf604a843a9f6bc17714a657c83dd762f183173dcde983a3570fa679b2bc40017d40b24163cdc4167796 + languageName: node + linkType: hard + +"d3-interpolate@npm:1 - 3": + version: 3.0.1 + resolution: "d3-interpolate@npm:3.0.1" + dependencies: + d3-color: "npm:1 - 3" + checksum: 988d66497ef5c190cf64f8c80cd66e1e9a58c4d1f8932d776a8e3ae59330291795d5a342f5a97602782ccbef21a5df73bc7faf1f0dc46a5145ba6243a82a0f0e + languageName: node + linkType: hard + +"d3-selection@npm:2 - 3, d3-selection@npm:3, d3-selection@npm:^3.0.0": + version: 3.0.0 + resolution: "d3-selection@npm:3.0.0" + checksum: 0e5acfd305b31628b7be5009ba7303d84bb34817a88ed4dde9c8bd9c23528573fc5272f89fc04e5be03d2cbf5441a248d7274aaf55a8ef3dad46e16333d72298 + languageName: node + linkType: hard + +"d3-timer@npm:1 - 3": + version: 3.0.1 + resolution: "d3-timer@npm:3.0.1" + checksum: 004128602bb187948d72c7dc153f0f063f38ac7a584171de0b45e3a841ad2e17f1e40ad396a4af9cce5551b6ab4a838d5246d23492553843d9da4a4050a911e2 + languageName: node + linkType: hard + +"d3-transition@npm:2 - 3": + version: 3.0.1 + resolution: "d3-transition@npm:3.0.1" + dependencies: + d3-color: "npm:1 - 3" + d3-dispatch: "npm:1 - 3" + d3-ease: "npm:1 - 3" + d3-interpolate: "npm:1 - 3" + d3-timer: "npm:1 - 3" + peerDependencies: + d3-selection: 2 - 3 + checksum: 02571636acb82f5532117928a87fe25de68f088c38ab4a8b16e495f0f2d08a3fd2937eaebdefdfcf7f1461545524927d2632d795839b88d2e4c71e387aaaffac + languageName: node + linkType: hard + +"d3-zoom@npm:^3.0.0": + version: 3.0.0 + resolution: "d3-zoom@npm:3.0.0" + dependencies: + d3-dispatch: "npm:1 - 3" + d3-drag: "npm:2 - 3" + d3-interpolate: "npm:1 - 3" + d3-selection: "npm:2 - 3" + d3-transition: "npm:2 - 3" + checksum: 0e6e5c14e33c4ecdff311a900dd037dea407734f2dd2818988ed6eae342c1799e8605824523678bd404f81e37824cc588f62dbde46912444c89acc7888036c6b + languageName: node + linkType: hard + "debounce@npm:^1.2.1": version: 1.2.1 resolution: "debounce@npm:1.2.1" @@ -13185,6 +13649,23 @@ __metadata: languageName: node linkType: hard +"reactflow@npm:^11.10.1": + version: 11.10.1 + resolution: "reactflow@npm:11.10.1" + dependencies: + "@reactflow/background": "npm:11.3.6" + "@reactflow/controls": "npm:11.2.6" + "@reactflow/core": "npm:11.10.1" + "@reactflow/minimap": "npm:11.7.6" + "@reactflow/node-resizer": "npm:2.2.6" + "@reactflow/node-toolbar": "npm:1.3.6" + peerDependencies: + react: ">=17" + react-dom: ">=17" + checksum: c4ecb93da678859669fb5b5fda8ba029742b9136dfd8bad5495be50f8f390a1d029543d3a010cd1751386a421756712e73511c4c0328d01f3629f2ef28aa0243 + languageName: node + linkType: hard + "readable-stream@npm:^2.0.1": version: 2.3.8 resolution: "readable-stream@npm:2.3.8" @@ -15164,6 +15645,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:1.2.0": + version: 1.2.0 + resolution: "use-sync-external-store@npm:1.2.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: a676216affc203876bd47981103f201f28c2731361bb186367e12d287a7566763213a8816910c6eb88265eccd4c230426eb783d64c373c4a180905be8820ed8e + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -15466,6 +15956,7 @@ __metadata: react: "npm:^18.2.0" react-chartjs-2: "npm:^5.2.0" react-dom: "npm:^18.2.0" + reactflow: "npm:^11.10.1" styled-components: "npm:^6.1.1" terser-webpack-plugin: "npm:^5.3.9" typescript: "npm:5.3.2" @@ -15722,6 +16213,26 @@ __metadata: languageName: node linkType: hard +"zustand@npm:^4.4.1": + version: 4.4.7 + resolution: "zustand@npm:4.4.7" + dependencies: + use-sync-external-store: "npm:1.2.0" + peerDependencies: + "@types/react": ">=16.8" + immer: ">=9.0" + react: ">=16.8" + peerDependenciesMeta: + "@types/react": + optional: true + immer: + optional: true + react: + optional: true + checksum: 572e42d912362eb3aabde9a37bca47caa547391fc4563e41444d9aef7ca453ca730ee1f00aae4b25c2635ca293d69c23ab0a41688cd4db8394545732a2d9c236 + languageName: node + linkType: hard + "zwitch@npm:^2.0.0": version: 2.0.4 resolution: "zwitch@npm:2.0.4"