diff --git a/site/docs/nodejs/CommandHandler/_category_.json b/site/docs/nodejs/CommandHandler/_category_.json new file mode 100644 index 000000000..384da5c64 --- /dev/null +++ b/site/docs/nodejs/CommandHandler/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Command Handler", + "position": 7 +} diff --git a/site/docs/nodejs/CommandHandler/command-manager.md b/site/docs/nodejs/CommandHandler/command-manager.md new file mode 100644 index 000000000..700d6be5e --- /dev/null +++ b/site/docs/nodejs/CommandHandler/command-manager.md @@ -0,0 +1,91 @@ +--- +sidebar_position: 2 +--- + +# Command Manager + +Currently, you probably have something like this in your code: + +```js +const Discord = require("discordeno"); +// Ideally you should move to an `.env` file +const config = require("./config.json"); + +const client = Discord.createBot({ + events: { + messageCreate(client, message) { + if (message.content === "!ping") { + client.helpers.sendMessage(message.channelId, { content: "pong" }); + } + }, + }, + intents: ["Guilds", "GuildMessages"], + token: config.token, +}); + +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/site/docs/nodejs/CommandHandler/create-command.md b/site/docs/nodejs/CommandHandler/create-command.md new file mode 100644 index 000000000..672d265ba --- /dev/null +++ b/site/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 been 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/site/docs/nodejs/CommandHandler/getting-started.md b/site/docs/nodejs/CommandHandler/getting-started.md new file mode 100644 index 000000000..622175240 --- /dev/null +++ b/site/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/site/docs/nodejs/EventHandler/_category_.json b/site/docs/nodejs/EventHandler/_category_.json new file mode 100644 index 000000000..32da17a11 --- /dev/null +++ b/site/docs/nodejs/EventHandler/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Event Handler", + "position": 6 +} diff --git a/site/docs/nodejs/EventHandler/event-manager.md b/site/docs/nodejs/EventHandler/event-manager.md new file mode 100644 index 000000000..1740f07d8 --- /dev/null +++ b/site/docs/nodejs/EventHandler/event-manager.md @@ -0,0 +1,119 @@ +--- +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 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/site/docs/nodejs/EventHandler/getting-started.md b/site/docs/nodejs/EventHandler/getting-started.md new file mode 100644 index 000000000..ef6d0e8c4 --- /dev/null +++ b/site/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. + +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. + +- 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. + +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/site/docs/nodejs/EventHandler/handle-event.md b/site/docs/nodejs/EventHandler/handle-event.md new file mode 100644 index 000000000..2ebe9632b --- /dev/null +++ b/site/docs/nodejs/EventHandler/handle-event.md @@ -0,0 +1,74 @@ +--- +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, its 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 = new Message(client, payload); + + if (message.isBot) 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 = new Interaction(client, 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 = new User(client, 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/site/docs/nodejs/Structures/_category_.json b/site/docs/nodejs/Structures/_category_.json new file mode 100644 index 000000000..8bfdb91d2 --- /dev/null +++ b/site/docs/nodejs/Structures/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Structures", + "position": 5 +} diff --git a/site/docs/nodejs/Structures/components.md b/site/docs/nodejs/Structures/components.md new file mode 100644 index 000000000..68651a284 --- /dev/null +++ b/site/docs/nodejs/Structures/components.md @@ -0,0 +1,219 @@ +--- +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/discordeno/discordeno/tree/main/template/nodejs/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/discordeno/discordeno/tree/main/template/nodejs/structures/Component.js). + +### Button + +```js +const button = new Component() + .setType("BUTTON") + .setStyle("LINK") + .setLabel("Click me!") + .setUrl("https://google.com") + .toJSON(); + +// Button with raw types +const button2 = new Component() + .setType(2) + .setStyle(4) + .setLabel("DO NOT CLICK") + .setCustomId("12345") + .toJSON(); + +const actionRow = new Component() + .setType("ACTION_ROW") + .setComponents(button, button2) + .toJSON(); + +// Message to send +const messageOptions = { content: "hello", components: [actionRow] }; + +await client.helpers.sendMessage(channelId, messageOptions); // You can also use the Message Structure +``` + +As you can see, for simplicity you can use strings instead of numbers (types), which are hard to remember. + +### Select Menu + +```js +const selectMenu = new 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 Component() + .setType("ACTION_ROW") + .setComponents(selectMenu) + .toJSON(); + +const messageOptions = { content: "hello", components: [actionRow] }; + +client.helpers.sendMessage(channelId, messageOptions); // You can also use the Message Structure +``` + +### Text Input + +```js +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(); + +new Interaction(client, 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. + +:::note Collecting + +An `InteractionCollector` can also be used to handle prompts, which requires some tweaks, but will be added soon in the +guide and the template repo. + +::: diff --git a/site/docs/nodejs/Structures/create-structure.md b/site/docs/nodejs/Structures/create-structure.md new file mode 100644 index 000000000..d7b7808c2 --- /dev/null +++ b/site/docs/nodejs/Structures/create-structure.md @@ -0,0 +1,79 @@ +--- +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 +const Channel { + constructor(client, data) { + this.client = client; + this.id = data.id; + this.name = data.name; + } + + async send(options) { + return await 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, you'll likely choose to continue using special structures. Therefore why we +have ready-made structures in our template repo: + +- [Guild](https://github.com/discordeno/discordeno/tree/main/template/nodejs/structures/Guild.js) +- [Channel](https://github.com/discordeno/discordeno/tree/main/template/nodejs/structures/Channel.js) +- [Role](https://github.com/discordeno/discordeno/tree/main/template/nodejs/structures/Role.js) +- [Member](https://github.com/discordeno/discordeno/tree/main/template/nodejs/structures/Member.js) +- [User](https://github.com/discordeno/discordeno/tree/main/template/nodejs/structures/User.js) +- [Message](https://github.com/discordeno/discordeno/tree/main/template/nodejs/structures/Message.js) +- [Interaction](https://github.com/discordeno/discordeno/tree/main/template/nodejs/structures/Interaction.js) + +We recommend that you clone the whole template repo, since some structures are based on other files. + +**Using the Structures:** + +```js +const Guild = require("./structures/Guild"); // Path to your structure +const guild = new Guild(client, data); // DiscordenoClient and DiscordenoPayloadData +``` + +Some popular methods have been added to the structures so that you can use them without having to come up with your own. +Of course, you can add your own methods and customize the structures to fit your needs. + +Next we're going to give a better insight into how create [`Embeds`](embeds) and [`Components`](components) with the +template structures. diff --git a/site/docs/nodejs/Structures/embeds.md b/site/docs/nodejs/Structures/embeds.md new file mode 100644 index 000000000..48015d54e --- /dev/null +++ b/site/docs/nodejs/Structures/embeds.md @@ -0,0 +1,107 @@ +--- +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. +[We also have a Template for this](https://github.com/discordeno/discordeno/tree/main/template/nodejs/structures/Embed.js) + +### Using the Embed Structure: + +```js +const Embed = require("./structures/Embed"); // Path to structure +const Channel = require("./structures/Channel"); // Path to structure + +const channel = new Channel(client, data); +const showCaseEmbed = new 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/site/docs/nodejs/Structures/getting-started.md b/site/docs/nodejs/Structures/getting-started.md new file mode 100644 index 000000000..1898e757b --- /dev/null +++ b/site/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/site/docs/nodejs/_category_.json b/site/docs/nodejs/_category_.json new file mode 100644 index 000000000..b45c7e9ac --- /dev/null +++ b/site/docs/nodejs/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Nodejs", + "position": 3 +} diff --git a/site/docs/nodejs/create-application.md b/site/docs/nodejs/create-application.md new file mode 100644 index 000000000..b7197a8ba --- /dev/null +++ b/site/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/site/docs/nodejs/design.md b/site/docs/nodejs/design.md new file mode 100644 index 000000000..580e70c7b --- /dev/null +++ b/site/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/site/docs/nodejs/getting-started.md b/site/docs/nodejs/getting-started.md new file mode 100644 index 000000000..22a0548d2 --- /dev/null +++ b/site/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 which too large to migrate to a slightly +different language. + +This guide will help you making your first Discord Bot using Node.js or even migrating your Bot from a other Library. + +:::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/site/docs/nodejs/initial-setup.md b/site/docs/nodejs/initial-setup.md new file mode 100644 index 000000000..04ab56456 --- /dev/null +++ b/site/docs/nodejs/initial-setup.md @@ -0,0 +1,46 @@ +--- +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/site/docs/nodejs/installion.md b/site/docs/nodejs/installion.md new file mode 100644 index 000000000..8a991dec5 --- /dev/null +++ b/site/docs/nodejs/installion.md @@ -0,0 +1,29 @@ +--- +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. Go to your terminal and run the following command: + +```cli +$ npm install discordeno +``` diff --git a/site/docs/nodejs/slash-command.md b/site/docs/nodejs/slash-command.md new file mode 100644 index 000000000..a0d096e8c --- /dev/null +++ b/site/docs/nodejs/slash-command.md @@ -0,0 +1,64 @@ +--- +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 add 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: ["Guilds"], + 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/template/nodejs/Managers/ChannelManager.js b/template/nodejs/Managers/ChannelManager.js new file mode 100644 index 000000000..6258d73f9 --- /dev/null +++ b/template/nodejs/Managers/ChannelManager.js @@ -0,0 +1,17 @@ +const Channel = require("../Structures/Channel"); +class Channels { + constructor(client, data = {}, options = {}) { + this.client = client; + + if (options.guild) this.guild = options.guild; + } + + async create(options = {}, reason) { + return new Channel(this.client, options).create(options, reason); + } + + forge(data = {}) { + return new Channel(this.client, data); + } +} +module.exports = Channels; diff --git a/template/nodejs/Managers/CommandManager.js b/template/nodejs/Managers/CommandManager.js new file mode 100644 index 000000000..344a09524 --- /dev/null +++ b/template/nodejs/Managers/CommandManager.js @@ -0,0 +1,136 @@ +const resolveFolder = (folderName) => path.resolve(__dirname, ".", folderName); +const fs = require("fs"); +const path = require("path"); + +class CommandManager { + constructor(client) { + this.client = client; + this.cache = new Map(); + this.aliases = new Map(); + } + load(options = {}) { + const commandFolderPath = options.path || "../Plugins"; + const commandFolder = resolveFolder(commandFolderPath); + if (options.category === undefined) options.category = true; + if (options.plugins === undefined) options.plugins = true; + //PluginMode will iterate through all SubFolders + fs.readdirSync(commandFolder).map(async (dir) => { + if (dir.endsWith(".txt")) return; + if (!options.category && dir.endsWith(".js")) { + const commandPath = path.join(commandFolder, dir); + this.loadCommand(commandPath); + } else { + fs.readdirSync(path.join(commandFolder, dir)).map((cmd) => { + if (cmd.endsWith(".js") && !options.plugins) { + const commandPath = path.join(commandFolder, dir, cmd); + this.loadCommand(commandPath); + } else if (commandFolderPath === "../Plugins") { + if (cmd !== "commands") return; + fs.readdirSync(path.join(commandFolder, dir, cmd)).map((cmdfile) => { + if (!cmdfile.endsWith(".js")) return; + const commandPath = path.join(commandFolder, dir, cmd, cmdfile); + this.loadCommand(commandPath); + }); + } + }); + } + }); + } + + loadCommand(commandPath) { + const pull = require(path.join(commandPath)); + if (pull.name) { + pull.path = commandPath; + this.cache.set(pull.name, pull); + } + if (pull.aliases) { + pull.aliases.map((p) => this.aliases.set(p, pull)); + } + return pull; + } + + reloadCommand(commandName) { + const command = this.cache.get(commandName); + if (!command) return; + const commandPath = path.join(command.path); + delete require.cache[require.resolve(commandPath)]; + return this.loadCommand(commandPath); + } + + isCommand(message) { + if (message.isBot) return false; + const prefix = "!"; + const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const prefixRegex = new RegExp(`^(<@!?${this.client.id}>|${escapeRegex(prefix)})\\s*`); + if (!prefixRegex.test(message.content)) return false; + + const [, matchedPrefix] = message.content.match(prefixRegex); + const args = message.content.slice(matchedPrefix.length).trim().split(/ +/); + + this.onMessage(message, prefix, args); + return true; + } + + isInteraction(interaction) { + if (interaction.type !== 2) return; + this.onInteraction(interaction); + } + + async onMessage(message, guild, args) { + const commandName = args.shift().toLowerCase(); + const command = this.cache.get(commandName); //|| this.cache.find(cmd => cmd.aliases && cmd.aliases.includes(commandName)); + if (!command && message.content.includes(this.client.id)) { + //Handle, when Command has not been found + const options = { content: "I did not found the Command!" }; + this.client.helpers.sendMessage(message.channelId, options); + } + if (!command) return; + + const messagecommand = new command({ + manager: this, + message: message, + client: this.client, + args: args, + settings: {}, + commandName: command.name, + }); + messagecommand.execute()?.catch?.((error) => { + console.log(error); + // Call Function on CommandResponse.js, handle the error + return messagecommand.onError(error ?? "custom"); + }); + } + + async onInteraction(interaction) { + const command = this.cache.get(interaction.data.name); + if (!command) return; + + const args = []; + //Map all Values and Args + interaction.data.options.map((o) => { + if (o.name) args.push(o.name); + if (o.options) { + o.options.map((o2) => { + if (o2.value) return args.push(o2.value); + if (o2.name) args.push(o2.name); + if (o2.options) o2.options.map((v) => args.push(v.value)); + }); + } + }); + + const messagecommand = new command({ + manager: this, + interaction: interaction, + client: this.client, + args: args, + settings: {}, + commandName: command.name, + }); + messagecommand.execute()?.catch?.((error) => { + console.log(error); + // Call Function on CommandResponse.js, handle the error + return messagecommand.onError(error ?? "custom"); + }); + } +} +module.exports = CommandManager; diff --git a/template/nodejs/Managers/EventManager.js b/template/nodejs/Managers/EventManager.js new file mode 100644 index 000000000..5d90bb0c2 --- /dev/null +++ b/template/nodejs/Managers/EventManager.js @@ -0,0 +1,32 @@ +const fs = require("fs"); +const path = require("path"); +const resolveFolder = (folderName) => path.resolve(__dirname, ".", folderName); + +const EventEmitter = require("events"); + +class EventManager extends EventEmitter { + constructor(client) { + super(); + 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; + /* When the event should be emitted on client.events.on(eventName, (...args) => {...}) + this._events[`${eventName}`] = function(...args) { + this.emit(eventName, ...args); + return event(...args); + }; + */ + }); + return this._events; + } +} +module.exports = EventManager; diff --git a/template/nodejs/Managers/MemberManager.js b/template/nodejs/Managers/MemberManager.js new file mode 100644 index 000000000..ce719cc48 --- /dev/null +++ b/template/nodejs/Managers/MemberManager.js @@ -0,0 +1,13 @@ +const Member = require("../Structures/Member"); +class Members { + constructor(client, data = {}, options = {}) { + this.client = client; + + if (options.guild) this.guild = options.guild; + } + + forge(data = {}) { + return new Member(this.client, data, { guild: this.guild }); + } +} +module.exports = Members; diff --git a/template/nodejs/Managers/RoleManager.js b/template/nodejs/Managers/RoleManager.js new file mode 100644 index 000000000..64997d1f9 --- /dev/null +++ b/template/nodejs/Managers/RoleManager.js @@ -0,0 +1,29 @@ +const Role = require("../Structures/Role"); +class Roles { + constructor(client, data = {}, options = {}) { + this.client = client; + if (options.member) this.member = options.member; + if (options.guild) this.guild = options.guild; + } + + async create(options = {}, reason) { + return new Role(this.client, options).create(options, reason); + } + + forge(data = {}) { + return new Roles(this.client, data); + } + + async add(options = {}, reason) { + const guildId = (this.guild ? this.guild.id : options.guildId); + const memberId = (this.member ? this.member.id : options.memberId); + return this.client.helpers.addRole(guildId, memberId, options.roleId, reason); + } + + async remove(options = {}, reason) { + const guildId = (this.guild ? this.guild.id : options.guildId); + const memberId = (this.member ? this.member.id : options.memberId); + return this.client.helpers.removeRole(guildId, memberId, options.roleId, reason); + } +} +module.exports = Roles; diff --git a/template/nodejs/Plugins/Developer/commands/eval.js b/template/nodejs/Plugins/Developer/commands/eval.js new file mode 100644 index 000000000..8544b879e --- /dev/null +++ b/template/nodejs/Plugins/Developer/commands/eval.js @@ -0,0 +1,49 @@ +const BaseCommand = require("../../../Structures/BaseCommand.js"); +const Embed = require("../../../Structures/Embed.js"); +class evalcommand extends BaseCommand { + static name = "eval"; + static description = "danger !!!"; + static category = "Developer"; + static slash = { name: "eval", category: "dev" }; + constructor(data) { + super(data); + } + async execute() { + if (!this.client.config.owners.includes(String(this.user.id))) return; + if (!(this.args.length > 0)) return this.reply({ content: "**You must provide something to eval!**" }); + + let inputOfEval = this.args.join(" "); + let outputOfEval; + let typeOfEval; + + try { + if (this.args.includes("await")) { + outputOfEval = await eval("(async () => {" + inputOfEval + "})()"); + } else { + outputOfEval = await eval(inputOfEval); + } + } catch (e) { + outputOfEval = e.message; + typeOfEval = e.name; + } + + var seen = []; + outputOfEval = typeof outputOfEval === "object" + ? JSON.stringify(outputOfEval, (_, value) => { + if (value == `Bot ${this.client.config.token}`) return `BOT_TOKEN`; + if (typeof value === "bigint") value = value.toString(); + if (typeof value === "object" && value !== null) { + if (seen.indexOf(value) !== -1) return; + else seen.push(value); + } + return value; + }, 1) + : outputOfEval; + + const embed = new Embed() + .addField({ name: "Input", value: "```js\n" + inputOfEval + "```" }) + .addField({ name: "Output", value: "```json\n" + `${outputOfEval}`.slice(0, 1000) + "```" }); + this.reply({ embeds: [embed] }); + } +} +module.exports = evalcommand; diff --git a/template/nodejs/Plugins/Developer/commands/reload.js b/template/nodejs/Plugins/Developer/commands/reload.js new file mode 100644 index 000000000..4e01117a7 --- /dev/null +++ b/template/nodejs/Plugins/Developer/commands/reload.js @@ -0,0 +1,19 @@ +const BaseCommand = require("../../../Structures/BaseCommand.js"); +const Embed = require("../../../Structures/Embed.js"); +class reloadcommand extends BaseCommand { + static name = "reload"; + static description = "Reloads a Command"; + static category = "Developer"; + static slash = { name: "reload", category: "dev" }; + constructor(data) { + super(data); + } + async execute() { + if (!this.client.config.owners.includes(String(this.user.id))) return; + if (!this.args[0]) return this.reply({ content: "**You must provide a command to reload!**" }); + const op = this.client.commands.reloadCommand(this.args[0]); + if (!op) return this.reply({ content: "**That command doesn't exist!**" }); + return this.reply({ content: "**Reloaded Command: `" + this.args[0] + "`**" }); + } +} +module.exports = reloadcommand; diff --git a/template/nodejs/Plugins/Developer/index.js b/template/nodejs/Plugins/Developer/index.js new file mode 100644 index 000000000..e69de29bb diff --git a/template/nodejs/Plugins/General/commands/ping.js b/template/nodejs/Plugins/General/commands/ping.js new file mode 100644 index 000000000..7a3666833 --- /dev/null +++ b/template/nodejs/Plugins/General/commands/ping.js @@ -0,0 +1,24 @@ +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 msg.edit({ embeds: [embed] }); + } +} +module.exports = pingcommand; diff --git a/template/nodejs/Plugins/General/index.js b/template/nodejs/Plugins/General/index.js new file mode 100644 index 000000000..e69de29bb diff --git a/template/nodejs/Plugins/Moderation/commands/ban.js b/template/nodejs/Plugins/Moderation/commands/ban.js new file mode 100644 index 000000000..5d6a7d5f2 --- /dev/null +++ b/template/nodejs/Plugins/Moderation/commands/ban.js @@ -0,0 +1,40 @@ +const BaseCommand = require("../../../Structures/BaseCommand.js"); +const Component = require("../../../Structures/Component.js"); + +class bancommand extends BaseCommand { + static name = "ban"; + static description = "Ban a user from the server"; + static usage = ""; + static category = "Moderation"; + static slash = { name: "ban", category: "mod" }; + constructor(data) { + super(data); + } + async execute() { + //Show Case Modal + + // Because no permission system has not been added + if (!this.client.config.owners.includes(String(this.user.id))) return; + + const textinput = new Component() + .setType("TEXT_INPUT") + .setStyle("SHORT") + .setCustomId("t1") + .setLabel("User ID") + .setPlaceholder("User ID") + .setRequired(true) + .setMaxLength(20) + .setMinLength(1) + .setValue(this.args[0]) + .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(1).setComponents(textinput).toJSON(); + const actionrow2 = new Component().setType(1).setComponents(textinput2).toJSON(); + + this.interaction.popupModal({ customId: "ban_modal", title: "Ban User", components: [actionrow, actionrow2] }); + } +} +module.exports = bancommand; diff --git a/template/nodejs/Plugins/Moderation/index.js b/template/nodejs/Plugins/Moderation/index.js new file mode 100644 index 000000000..e69de29bb diff --git a/template/nodejs/Structures/BaseCommand.js b/template/nodejs/Structures/BaseCommand.js new file mode 100644 index 000000000..146f2d78f --- /dev/null +++ b/template/nodejs/Structures/BaseCommand.js @@ -0,0 +1,17 @@ +const UtilCommand = require("./CommandResponse.js"); +const Message = require("./Message.js"); +const Interaction = require("./Interaction.js"); +class BaseCommand extends UtilCommand { + constructor(data) { + super(data); + this.message = data.message && new Message(data.client, data.message); + this.interaction = data.interaction && new Interaction(data.client, data.interaction); + this.user = this.message ? this.message.author : this.interaction.user; + this.guild = this.message ? this.message.guild : this.interaction.guild; + this.member = this.message ? this.message.member : this.interaction.member; + this.channel = this.message ? this.message.channel : this.interaction.channel; + this.client = data.client; + this.settings = data.settings ?? {}; + } +} +module.exports = BaseCommand; diff --git a/template/nodejs/Structures/Channel.js b/template/nodejs/Structures/Channel.js new file mode 100644 index 000000000..0e18fd002 --- /dev/null +++ b/template/nodejs/Structures/Channel.js @@ -0,0 +1,29 @@ +const DestructObject = require("./DestructObject"); +const Guild = require("./Guild"); + +class Channel extends DestructObject { + constructor(client, channel = {}, options = {}) { + super(channel); + if (options.guild) this.guild = options.guild; + else if (channel.guildId) this.guild = new Guild(client, { id: channel.guildId }); + + this.client = client; + } + + async create(options = {}, reason) { + return this.client.helpers.createChannel(this.guildId, options, reason); + } + + async edit(options = {}, reason) { + return this.client.helpers.editChannel(this.id, options, reason); + } + + async delete(reason) { + return this.client.helpers.deleteChannel(this.id, reason); + } + + async send(options = {}) { + return this.client.helpers.sendMessage(this.id, options); + } +} +module.exports = Channel; diff --git a/template/nodejs/Structures/CommandResponse.js b/template/nodejs/Structures/CommandResponse.js new file mode 100644 index 000000000..9c5b17c8f --- /dev/null +++ b/template/nodejs/Structures/CommandResponse.js @@ -0,0 +1,57 @@ +const Message = require("./Message"); + +class Responses { + constructor(data) { + this.manager = data.manager; + this.args = this._validateArguments(data.args); + this.replied = false; + } + + async reply(content) { + // When just a string is passed, we assume it's the content -> transform to correct formatted payload + if (typeof content === "string") content = { content }; + if (this.interaction) { + if (this.replied) return this.followUp(content); + const reply = await this.interaction.reply(content); + + //Assign properties to the response + const response = new Message(this.client, reply); + + this.replied = true; + return response; + } + if (this.message) { + if (this.replied) return this.followUp(content); + + const msg = await this.message.channel.send(content); + + //Assign properties to the response + const response = new Message(this.client, msg); + this.replied = true; + return response; + } + } + + async followUp(content) { + if (this.interaction) { + const reply = await this.interaction.followUp(content); + const response = new Message(this.client, reply); + return response; + } + if (this.message) { + const msg = await this.message.channel.send(content); + const response = new Message(this.client, msg); + return response; + } + } + + onError(error) { + return this.reply({ content: `A unknown Error happend: \n> ${error}` }); + } + + _validateArguments(args) { + this.args = args; + return args; + } +} +module.exports = Responses; diff --git a/template/nodejs/Structures/Component.js b/template/nodejs/Structures/Component.js new file mode 100644 index 000000000..31dcfc9d1 --- /dev/null +++ b/template/nodejs/Structures/Component.js @@ -0,0 +1,171 @@ +const Constants = { + PRIMARY: 1, + SECONDARY: 2, + SUCCESS: 3, + DANGER: 4, + LINK: 5, + SHORT: 1, + PARAGRAPH: 2, + + ACTION_ROW: 1, + BUTTON: 2, + SELECT_MENU: 3, + TEXT_INPUT: 4, +}; +class Component { + constructor(options = {}) { + this.type = options.type; + this.custom_id = options.custom_id ?? options.customId; + this.disabled = options.disabled; + this.style = options.style; + this.label = options.label; + this.emoji = options.emoji; + this.url = options.url; + + //Select Menu + this.options = options.options; + this.placeholder = options.placeholder; + this.min_values = options.min_values ?? options.minValues; + this.max_values = options.max_values ?? options.maxValues; + + //Action Row + this.components = options.components; + + //Modal + this.value = options.value; + this.min_length = options.min_length ?? options.minLength; + this.max_length = options.max_length ?? options.maxLength; + this.required = options.required; + } + + setType(type) { + if (typeof type === "string") { + this.type = Constants[type.toUpperCase()]; + if (!this.type) throw new Error(`Invalid Component Type: ${type}`); + } else this.type = type; + return this; + } + + setCustomId(custom_id) { + if (!this.url) this.custom_id = custom_id; + return this; + } + + setDisabled(disabled) { + this.disabled = disabled; + return this; + } + + setRequired(required) { + this.required = required; + return this; + } + + setStyle(style) { + if (!this.url) { + if (typeof style === "string") { + this.style = Constants[style.toUpperCase()]; + if (!this.style) throw new Error(`Invalid Button Style Type: ${type}`); + } else this.style = style; + } + return this; + } + + setLabel(label) { + this.label = label; + return this; + } + + setEmoji(emoji) { + this.emoji = emoji; + return this; + } + + setUrl(url) { + this.url = url; + this.style = 5; + this.custom_id = undefined; + return this; + } + + setOptions(options) { + this.options = options; + return this; + } + + setValue(value) { + this.value = value; + return this; + } + + setPlaceholder(placeholder) { + this.placeholder = placeholder; + return this; + } + + setMinValues(min_values) { + this.min_values = min_values; + return this; + } + + setMaxValues(max_values) { + this.max_values = max_values; + return this; + } + + setMinLength(min_values) { + this.min_length = min_values; + return this; + } + + setMaxLength(max_values) { + this.max_length = max_values; + return this; + } + + setComponents(...components) { + this.components = components; + return this; + } + + toJSON() { + if (!this.type) throw new Error("Component must have a type"); + const json = { + type: this.type, + }; + if (this.type === 1) { + json.components = this.components; + } + + if (this.type === 2) { + json.customId = this.custom_id; + json.style = this.style; + json.label = this.label; + json.emoji = this.emoji; + json.url = this.url; + json.disabled = this.disabled; + } + + if (this.type === 3) { + json.customId = this.custom_id; + json.options = this.options; + json.placeholder = this.placeholder; + json.minValues = this.min_values; + json.maxValues = this.max_values; + json.disabled = this.disabled; + } + + if (this.type === 4) { + json.customId = this.custom_id; + json.style = this.style; + json.label = this.label; + json.minLength = this.min_length; + json.maxLength = this.max_length; + json.required = this.required; + json.value = this.value; + json.placeholder = this.placeholder; + } + return json; + } +} +module.exports = Component; diff --git a/template/nodejs/Structures/DestructObject.js b/template/nodejs/Structures/DestructObject.js new file mode 100644 index 000000000..2f0dff7b0 --- /dev/null +++ b/template/nodejs/Structures/DestructObject.js @@ -0,0 +1,16 @@ +class DestructObject { + constructor(message = {}) { + this.destructObject(message); + } + destructObject(message) { + for (let [key, value] of Object.entries(message)) { + this[key] = value; + } + return this; + } + + toJSON() { + return { ...this }; + } +} +module.exports = DestructObject; diff --git a/template/nodejs/Structures/Embed.js b/template/nodejs/Structures/Embed.js new file mode 100644 index 000000000..cb7e88ba0 --- /dev/null +++ b/template/nodejs/Structures/Embed.js @@ -0,0 +1,99 @@ +class Embed { + constructor(options = {}) { + this.title = options.title; + this.description = options.description; + this.fields = options.fields; + this.thumbnail = options.thumbnail; + this.image = options.image; + this.author = options.author; + this.color = options.color; + this.timestamp = options.timestamp; + this.footer = options.footer; + this.url = options.url; + this.fields = options.fields ?? []; + } + setTitle(title) { + this.title = title; + return this; + } + + setDescription(description) { + this.description = description; + return this; + } + + setThumbnail(thumbnail) { + this.thumbnail = thumbnail; + return this; + } + + setImage(image) { + this.image = image; + return this; + } + + setAuthor(author) { + if (typeof author !== "object") throw new Error("Author must be an object"); + this.author = author; + return this; + } + + setColor(color) { + this.color = color; + return this; + } + + setTimestamp(timestamp) { + this.timestamp = timestamp ?? Date.now(); + return this; + } + + setFooter(footer) { + if (typeof footer !== "object") throw new Error("Footer must be an object"); + this.footer = footer; + return this; + } + + setURL(url) { + this.url = url; + return this; + } + + addField(field) { + this.fields.push(field); + return this; + } + + addFields(...fields) { + fields.map((x) => this.addField(x)); + return this; + } + + toJSON() { + return { + title: this.title, + type: "rich", + description: this.description, + color: this.color, + timestamp: this.timestamp ? new Date(this.timestamp).toISOString() : null, + thumbnail: this.thumbnail, + image: this.image, + fields: this.fields, + url: this.url, + author: this.author + ? { + name: this.author.name, + url: this.author.url, + iconUrl: this.author.icon_url || this.author.iconUrl, + } + : null, + footer: this.footer + ? { + text: this.footer.text, + iconUrl: this.footer.icon_url || this.footer.iconUrl, + } + : null, + }; + } +} +module.exports = Embed; diff --git a/template/nodejs/Structures/Guild.js b/template/nodejs/Structures/Guild.js new file mode 100644 index 000000000..fe3a19d1f --- /dev/null +++ b/template/nodejs/Structures/Guild.js @@ -0,0 +1,18 @@ +const DestructObject = require("./DestructObject"); + +const RoleManager = require("../Managers/RoleManager"); +const MemberManager = require("../Managers/MemberManager"); +const ChannelManager = require("../Managers/ChannelManager"); + +class Guild extends DestructObject { + constructor(client, guild = {}) { + super(guild); + this.client = client; + + //Managers: + this.roles = new RoleManager(client, {}, { guild: this }); + this.members = new MemberManager(client, {}, { guild: this }); + this.channels = new ChannelManager(client, {}, { guild: this }); + } +} +module.exports = Guild; diff --git a/template/nodejs/Structures/Interaction.js b/template/nodejs/Structures/Interaction.js new file mode 100644 index 000000000..a21b8123f --- /dev/null +++ b/template/nodejs/Structures/Interaction.js @@ -0,0 +1,116 @@ +const DestructObject = require("./DestructObject"); +const Guild = require("./Guild"); +const Channel = require("./Channel"); +const Constants = { + DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE: 5, + CHANNEL_MESSAGE_WITH_SOURCE: 4, + DEFERRED_UPDATE_MESSAGE: 6, + UPDATE_MESSAGE: 7, + APPLICATION_COMMAND_AUTOCOMPLETE_RESULT: 8, + MODAL: 9, + FLAGS: { EPHEMERAL: 64 }, + INTERACTION_TYPES: { + CHAT_INPUT: 1, + APPLICATION_COMMAND: 2, + CONTEXT_MENU: 2, + MESSAGE_COMPONENT: 3, + APPLICATION_COMMAND_AUTOCOMPLETE: 4, + }, +}; +class Interaction extends DestructObject { + constructor(client, interaction = {}) { + super(interaction); + this.raw = interaction; + this.client = client; + + this.guild = new Guild(client, { id: this.guild_id || this.guildId }); + this.channel = new Channel(client, { id: this.channel_id || this.channelId }, { + internal: true, + guild: this.guild, + }); + } + + isCommand() { + return this.type === Constants.INTERACTION_TYPES.APPLICATION_COMMAND; + } + + // @todo check Context Menu type and Component Type + isChatInputCommand() { + return this.type === Constants.INTERACTION_TYPES.CHAT_INPUT; + } + isContextMenuCommand() { + return this.isCommand(); + } + isAutoComplete() { + return this.type === Constants.INTERACTION_TYPES.APPLICATION_COMMAND_AUTOCOMPLETE; + } + isMessageComponent() { + return this.type === Constants.INTERACTION_TYPES.MESSAGE_COMPONENT; + } + isSelectMenu() { + return this.type === Constants.INTERACTION_TYPES.MESSAGE_COMPONENT; + } + isButton() { + return this.type === Constants.INTERACTION_TYPES.MESSAGE_COMPONENT; + } + + async deferReply(options = {}) { + if (this.deferred || this.replied) throw new Error("Interaction has been already replied"); + const Payload = { data: {}, type: Constants.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE }; + options.type = 5; + if (this.ephemeral) options.private = true; + this.ephemeral = options.ephemeral || false; + this.deferred = true; + return this.client.helpers.sendInteractionResponse(this.id, this.token, options); + } + + async deferUpdate(options = {}) { + if (this.deferred || this.replied) throw new Error("Interaction has been already replied"); + this.deferred = true; + const Payload = { data: options, type: Constants.DEFERRED_UPDATE_MESSAGE }; + return this.client.helpers.sendInteractionResponse(this.id, this.token, Payload); + } + + async reply(options = {}) { + if (this.deferred || this.replied) throw new Error("Interaction has been already replied"); + this.ephemeral = options.ephemeral || false; + if (this.ephemeral) options.private = true; + this.replied = true; + const Payload = { data: options, type: Constants.CHANNEL_MESSAGE_WITH_SOURCE }; + return this.client.helpers.sendInteractionResponse(this.id, this.token, Payload); + } + + async popupModal(options = {}) { + if (this.deferred || this.replied) throw new Error("Interaction has been already replied"); + const Payload = { data: options, type: Constants.MODAL }; + this.replied = true; + return this.client.helpers.sendInteractionResponse(this.id, this.token, Payload); + } + + async editReply(options = {}) { + if (!this.deferred && !this.replied) throw new Error("Interaction has not been replied"); + this.replied = true; + const messageId = this.messageId ? this.messageId : options.messageId; + return this.client.helpers.editInteractionResponse(this.token, options); + } + + async deleteReply(options = {}) { + if (this.ephemeral) throw new Error("Ephemeral messages cannot be deleted"); + const messageId = this.messageId ? this.messageId : options.messageId; + return this.client.helpers.deleteInteractionResponse(this.token, messageId); + } + + async followUp(options = {}) { + if (!this.replied || !this.deferred) throw new Error("Interaction has not been replied"); + const Payload = { data: options, type: Constants.CHANNEL_MESSAGE_WITH_SOURCE }; + return this.client.helpers.sendInteractionResponse(this.id, this.token, Payload); + } + + async update(options = {}) { + if (this.deferred || this.replied) throw new Error("Interaction has been already replied"); + const Payload = { data: options, type: Constants.UPDATE_MESSAGE }; + this.replied = true; + return this.client.helpers.sendInteractionResponse(this.id, this.token, Payload); + } +} +module.exports = Interaction; diff --git a/template/nodejs/Structures/Member.js b/template/nodejs/Structures/Member.js new file mode 100644 index 000000000..2aab97af5 --- /dev/null +++ b/template/nodejs/Structures/Member.js @@ -0,0 +1,16 @@ +const DestructObject = require("./DestructObject"); +const Guild = require("./Guild"); +const RoleManager = require("../Managers/RoleManager"); + +class Member extends DestructObject { + constructor(client, member = {}, options = {}) { + super(member); + this.client = client; + + if (options.guild) this.guild = options.guild; + else if (member.guildId) this.guild = new Guild(client, { id: member.guildId }); + + this.roles = new RoleManager(client, {}, { guild: this.guild, member: this }); + } +} +module.exports = Member; diff --git a/template/nodejs/Structures/Message.js b/template/nodejs/Structures/Message.js new file mode 100644 index 000000000..39d46fb63 --- /dev/null +++ b/template/nodejs/Structures/Message.js @@ -0,0 +1,50 @@ +const DestructObject = require("./DestructObject"); + +const Channel = require("./Channel"); +const Guild = require("./Guild"); +const Member = require("./Member"); +const User = require("./User"); + +class Message extends DestructObject { + constructor(client, message = {}) { + super(message); + this.client = client; + this.guild = new Guild(client, { id: this.guild_id || this.guildId }); + this.channel = new Channel(client, { id: this.channel_id || this.channelId }, { guild: this.guild }); + this.member = new Member(client, message.member, { guild: this.guild }); + this.author = new User(client, { + id: this.author_id || this.authorId, + username: this.tag?.split("#")[0], + discriminator: this.tag?.split("#")[1], + bot: this.isBot, + }); + } + + async edit(options) { + return this.client.helpers.editMessage(this.channel.id, this.id, options); + } + + async reply(options = {}) { + if (!options.messageReference) { + options.messageReference = { messageId: this.id, channelId: this.channel.id, guildId: this.guild.id }; + } + return this.client.helpers.sendMessage(this.channel.id, options); + } + + async delete(options = {}) { + return this.client.helpers.deleteMessage(this.channel.id, this.id, options.reason, options.delayMilliseconds); + } + + async react(emoji) { + return this.client.helpers.addReaction(this.channel.id, this.id, emoji); + } + + async pin() { + return this.client.helpers.pinMessage(this.channel.id, this.id); + } + + async unpin() { + return this.client.helpers.unpinMessage(this.channel.id, this.id); + } +} +module.exports = Message; diff --git a/template/nodejs/Structures/Permissions.js b/template/nodejs/Structures/Permissions.js new file mode 100644 index 000000000..1ed537b37 --- /dev/null +++ b/template/nodejs/Structures/Permissions.js @@ -0,0 +1,9 @@ +/// On work +class Permissions { + constructor(client, data = {}, options = {}) { + this.client = client; + } + + has(bit) { + } +} diff --git a/template/nodejs/Structures/Role.js b/template/nodejs/Structures/Role.js new file mode 100644 index 000000000..8e92ad114 --- /dev/null +++ b/template/nodejs/Structures/Role.js @@ -0,0 +1,12 @@ +const DestructObject = require("./DestructObject"); +const Guild = require("./Guild"); + +class Role extends DestructObject { + constructor(client, role = {}, options = {}) { + super(role); + if (options.guild) this.guild = options.guild; + else if (role.guildId) this.guild = new Guild(client, { id: role.guildId }); + this.client = client; + } +} +module.exports = Role; diff --git a/template/nodejs/Structures/User.js b/template/nodejs/Structures/User.js new file mode 100644 index 000000000..7626a3dfa --- /dev/null +++ b/template/nodejs/Structures/User.js @@ -0,0 +1,13 @@ +const DestructObject = require("./DestructObject"); + +class User extends DestructObject { + constructor(client, user = {}) { + super(user); + this.client = client; + } + + get tag() { + return `#${this.username}#${this.discriminator}`; + } +} +module.exports = User; diff --git a/template/nodejs/events/interactionCreate.js b/template/nodejs/events/interactionCreate.js new file mode 100644 index 000000000..ff76be71e --- /dev/null +++ b/template/nodejs/events/interactionCreate.js @@ -0,0 +1,3 @@ +module.exports = async (client, interaction) => { + client.commands.isInteraction(interaction); +}; diff --git a/template/nodejs/events/messageCreate.js b/template/nodejs/events/messageCreate.js new file mode 100644 index 000000000..3b18d1977 --- /dev/null +++ b/template/nodejs/events/messageCreate.js @@ -0,0 +1,3 @@ +module.exports = async (client, message) => { + client.commands.isCommand(message); +}; diff --git a/template/nodejs/events/ready.js b/template/nodejs/events/ready.js new file mode 100644 index 000000000..5e6fa6aab --- /dev/null +++ b/template/nodejs/events/ready.js @@ -0,0 +1,9 @@ +const User = require("../Structures/User"); +module.exports = async (client, payload) => { + client.user = new User(client, 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/template/nodejs/index.js b/template/nodejs/index.js new file mode 100644 index 000000000..10c3d81df --- /dev/null +++ b/template/nodejs/index.js @@ -0,0 +1,29 @@ +const Discord = require("discordeno"); + +// Ideally you should switch this to .env but for a template a config json is enough +const config = require("../config.json"); + +const EventManager = require("./Managers/EventManager.js"); +// looping through all events and registering them +const events = new EventManager({}); + +const client = Discord.createBot({ + events: events.load({}), + intents: ["Guilds", "GuildMessages"], + token: config.token, +}); + +client.config = config; + +// looping through all commands and registering them in .cache of the class +const CommandManager = require("./Managers/CommandManager.js"); +client.commands = new CommandManager(client); +client.commands.load({}); + +// Starts your Bot +Discord.startBot(client); + +/* +* You should handle all errors and fix the issues in your codes... +* process.on('unhandledRejection', (reason, p) => {console.log(reason, p)}) +*/