diff --git a/.eslintrc b/.eslintrc index 59674b61..3dbcf038 100755 --- a/.eslintrc +++ b/.eslintrc @@ -9,15 +9,15 @@ "rules": { "semi": "error", "quotes": "error", + "@typescript-eslint/no-empty-function": "warn", + "@typescript-eslint/no-unused-vars": "warn", + "@typescript-eslint/no-empty-interface": "warn", + "@typescript-eslint/no-inferrable-types": "warn", + "@typescript-eslint/no-non-null-asserted-optional-chain": "warn", + "@typescript-eslint/no-non-null-assertion": "warn", "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-empty-function": "off", - "@typescript-eslint/ban-types": "off", - "@typescript-eslint/no-unused-vars": "off", - "@typescript-eslint/no-empty-interface": "off", - "@typescript-eslint/no-inferrable-types": "off", "@typescript-eslint/no-var-requires": "off", - "@typescript-eslint/no-non-null-asserted-optional-chain": "off", - "@typescript-eslint/no-non-null-assertion": "off" + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/ban-types": "off" } } diff --git a/README.md b/README.md index d45180ed..1923e1c1 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,17 @@

# 🎻 Introduction + This module is an extension of **[discord.**js**](https://discordjs.guide/)**, so the internal behavior (methods, properties, ...) is the same. This library allows you to use TypeScript decorators on discord.**js**, it simplify your code and improve the readability ! # πŸ“œ Documentation + **[https://owencalvin.github.io/discord.ts/](https://owencalvin.github.io/discord.ts/)** # πŸ“Ÿ @Slash - Discord commands + Discord has it's own command system now, you can simply declare commands and use Slash commands this way ```ts @@ -46,7 +49,9 @@ abstract class AppDiscord { ``` ## Decorators related to Slash commands + There is a whole system that allows you to implement complex Slash commands + - `@Choice` - `@Choices` - `@Option` @@ -57,9 +62,10 @@ There is a whole system that allows you to implement complex Slash commands - `@Guard` # πŸ’‘@On / @Once - Discord events -We can declare methods that will be executed whenever a Discord event is triggered. -Our methods must be decorated with the `@On(event: string)` or `@Once(event: string)` decorator. +We can declare methods that will be executed whenever a Discord event is triggered. + +Our methods must be decorated with the `@On(event: string)` or `@Once(event: string)` decorator. That's simple, when the event is triggered, the method is called: @@ -68,7 +74,7 @@ import { Discord, On, Once } from "@typeit/discord"; @Discord() abstract class AppDiscord { - @On("message") + @On("messageCreate") private onMessage() { // ... } @@ -81,6 +87,7 @@ abstract class AppDiscord { ``` # βš”οΈ Guards + We implemented a guard system thats work pretty like the [Koa](https://koajs.com/) middleware system You can use functions that are executed before your event to determine if it's executed. For example, if you want to apply a prefix to the messages, you can simply use the `@Guard` decorator. @@ -96,12 +103,12 @@ import { Prefix } from "./Prefix"; @Discord() abstract class AppDiscord { - @On("message") + @On("messageCreate") @Guard( NotBot, // You can use multiple guard functions, they are excuted in the same order! Prefix("!") ) - async onMessage([message]: ArgsOf<"message">) { + async onMessage([message]: ArgsOf<"messageCreate">) { switch (message.content.toLowerCase()) { case "hello": message.reply("Hello!"); @@ -115,13 +122,13 @@ abstract class AppDiscord { ``` # πŸ“‘ Installation + Use [npm](https://www.npmjs.com/package/@typeit/discord) or yarn to install **@typeit/discord@slash** with **discord.js** **[Please refer to the documentation](https://owencalvin.github.io/discord.ts/installation/#installation)** - # ☎️ Need help? -**[Simply join the Discord server](https://discord.gg/VDjwu8E)** +**[Simply join the Discord server](https://discord.gg/VDjwu8E)** You can also find help with the [different projects that use discord.ts](https://github.com/OwenCalvin/discord.ts/network/dependents?package_id=UGFja2FnZS00Njc1MzYwNzU%3D) and in the [examples folder](https://github.com/OwenCalvin/discord.ts/tree/master/examples) diff --git a/docs/src/.vuepress/config.js b/docs/src/.vuepress/config.js index f7883258..b3704cc5 100755 --- a/docs/src/.vuepress/config.js +++ b/docs/src/.vuepress/config.js @@ -69,17 +69,21 @@ module.exports = { path: "/decorators/discord", children: [ ["/decorators/discord", "@Discord"], - ["/decorators/on", "@On"], - ["/decorators/once", "@Once"], - ["/decorators/slash", "@Slash"], - ["/decorators/option", "@Option"], + ["/decorators/bot", "@Bot"], + ["/decorators/button", "@Button"], ["/decorators/choice", "@Choice"], ["/decorators/choices", "@Choices"], - ["/decorators/guild", "@Guild"], - ["/decorators/permission", "@Permission"], + ["/decorators/description", "@Description"], ["/decorators/group", "@Group"], ["/decorators/guard", "@Guard"], - ["/decorators/description", "@Description"], + ["/decorators/guild", "@Guild"], + ["/decorators/on", "@On"], + ["/decorators/once", "@Once"], + ["/decorators/option", "@Option"], + ["/decorators/defaultpermission", "@DefaultPermission"], + ["/decorators/permission", "@Permission"], + ["/decorators/selectmenu", "@SelectMenu"], + ["/decorators/slash", "@Slash"], ] } ] diff --git a/docs/src/decorators/bot.md b/docs/src/decorators/bot.md new file mode 100644 index 00000000..b8b1f2aa --- /dev/null +++ b/docs/src/decorators/bot.md @@ -0,0 +1,5 @@ +# @Bot + +::: danger +doc for `@Bot` is not ready +::: diff --git a/docs/src/decorators/button.md b/docs/src/decorators/button.md new file mode 100644 index 00000000..53fb816d --- /dev/null +++ b/docs/src/decorators/button.md @@ -0,0 +1,5 @@ +# @Button + +::: danger +doc for `@Button` is not ready +::: diff --git a/docs/src/decorators/choice.md b/docs/src/decorators/choice.md index e5ab710b..dbd9b539 100644 --- a/docs/src/decorators/choice.md +++ b/docs/src/decorators/choice.md @@ -1,10 +1,12 @@ # @Choice + An option of a Slash command can implement an autocompletion feature for `string` and `number` types ![](/discord.ts/choices.png) ## Setup autocompletion -You just decorate your parameter with one or multiple @Choice ! + +You just decorate your parameter with one or multiple @Choice ! ```ts @Discord() @@ -25,12 +27,15 @@ class DiscordBot { ``` ## Params + `@Choice(name: string, value: string | number)` ### Name + `string` You have to set a diplayed name for your Choice ### Value + `string | number` You have to set a value for your choice, if the user select "Astraunot", you will receive the value "astro" diff --git a/docs/src/decorators/choices.md b/docs/src/decorators/choices.md index 5dfe5985..268972a7 100644 --- a/docs/src/decorators/choices.md +++ b/docs/src/decorators/choices.md @@ -1,4 +1,5 @@ # @Choices + It works exactly like [@Choice](/decorators/choice/) except that you can directly pass an object or enum to define all the choices at once > The key of the object or enum is what discord shows and the value is the property value (object[key]) @@ -7,7 +8,7 @@ It works exactly like [@Choice](/decorators/choice/) except that you can directl enum TextChoices { // WhatDiscordShows = value Hello = "Hello", - "Good Bye" = "GoodBye" + "Good Bye" = "GoodBye", } // Could be diff --git a/docs/src/decorators/defaultpermission.md b/docs/src/decorators/defaultpermission.md new file mode 100644 index 00000000..31edb1c9 --- /dev/null +++ b/docs/src/decorators/defaultpermission.md @@ -0,0 +1,5 @@ +# @DefaultPermission + +::: danger +doc for `@DefaultPermission` is not ready +::: diff --git a/docs/src/decorators/description.md b/docs/src/decorators/description.md index 17f0821e..65423a4e 100644 --- a/docs/src/decorators/description.md +++ b/docs/src/decorators/description.md @@ -1,9 +1,10 @@ # @Description + This decorator is a shortcut to set the description property ```typescript import { ClassCommand, Command, CommandMessage } from "@typeit/discord"; -import { CommandInteraction } from "discord.js" +import { CommandInteraction } from "discord.js"; @Discord() export abstract class DiscordBot { @@ -19,7 +20,7 @@ Is equivalent to: ```typescript import { ClassCommand, Command, CommandMessage } from "@typeit/discord"; -import { CommandInteraction } from "discord.js" +import { CommandInteraction } from "discord.js"; @Discord() export abstract class DiscordBot { diff --git a/docs/src/decorators/discord.md b/docs/src/decorators/discord.md index 6036aa12..faed9c0b 100644 --- a/docs/src/decorators/discord.md +++ b/docs/src/decorators/discord.md @@ -1,4 +1,5 @@ # @Discord + This decorator instanciate the class inside the discord.**ts** library to access to the class members or to call the methods ::: danger @@ -11,10 +12,9 @@ import { Discord, Slash } from "@typeit/discord"; @Discord() abstract class AppDiscord { // We can use member decorators - // because we decorated the class with @Discord - @Slash("hello") - private hello( - ) { + // because we decorated the class with @Discord + @Slash("hello") + private hello() { // ... } } diff --git a/docs/src/decorators/group.md b/docs/src/decorators/group.md index 890fedc1..92b9c673 100644 --- a/docs/src/decorators/group.md +++ b/docs/src/decorators/group.md @@ -1,5 +1,7 @@ # @Group + You can group your command like this + ``` command | @@ -8,6 +10,7 @@ command |__ subcommand ``` + ``` command | @@ -21,17 +24,21 @@ command ``` ## Example -Here you create a Slash command group that groups "permissions" commands + +Here you create a Slash command group that groups "permissions" commands The permissions commands also grouped by "user" or "role" ![](https://discord.com/assets/4cfea1bfc6d3ed0396c16cd47e0a7154.png) ## Create a Group + We use @Group at two level, on the class and on methods ### Group on class level + When @Group decorate a class it groups all the Slash commands in the class + ``` maths | @@ -42,10 +49,7 @@ maths ```ts @Discord() -@Group( - "maths", - "maths group description", -) +@Group("maths", "maths group description") export abstract class AppDiscord { @Slash("add") add( @@ -70,12 +74,15 @@ export abstract class AppDiscord { } } ``` + ![](/discord.ts/group1.png) ### Group on method level + When @Group decorate a method it creates sub-groups inside the class group **You have to list the groups that are in the class in the @Group parameters that decorate the class, or they will not appear** + ```ts @Group( "testing", @@ -102,16 +109,13 @@ testing | |__ root ``` + ```ts @Discord() -@Group( - "testing", - "Testing group description", - { - maths: "maths group description", - text: "text group description" - } -) +@Group("testing", "Testing group description", { + maths: "maths group description", + text: "text group description", +}) export abstract class AppDiscord { @Slash("add") @Group("maths") @@ -155,4 +159,3 @@ export abstract class AppDiscord { ``` ![](/discord.ts/group2.png) - diff --git a/docs/src/decorators/guard.md b/docs/src/decorators/guard.md index d2982bef..fabae3c5 100644 --- a/docs/src/decorators/guard.md +++ b/docs/src/decorators/guard.md @@ -1,10 +1,14 @@ # @Guard +::: warning +add example for slash, argof message does not apply on interactions +::: + You can use functions that are executed before your event to determine if it's executed. For example, if you want to apply a prefix to the messages, you can simply use the `@Guard` decorator. The order of execution of the guards is done according to their position in the list, so they will be executed in order (from top to bottom). -Guards can be set for `@Slash`, `@On`, `@Once`, `@Discord` and globaly. +Guards can be set for `@Slash`, `@Button`, `@SelectMenu`, `@On`, `@Once`, `@Discord` and globaly. ```typescript import { Discord, On, Client, Guard } from "@typeit/discord"; @@ -13,12 +17,12 @@ import { Prefix } from "./Prefix"; @Discord() abstract class AppDiscord { - @On("message") + @On("messageCreate") @Guard( NotBot, // You can use multiple guard functions, they are excuted in the same order! Prefix("!") ) - async onMessage([message]: ArgsOf<"message">) { + async onMessage([message]: ArgsOf<"messageCreate">) { switch (message.content.toLowerCase()) { case "hello": message.reply("Hello!"); @@ -49,8 +53,8 @@ import { Prefix } from "./Prefix"; @Discord() @Guard(NotBot, Prefix("!")) abstract class AppDiscord { - @On("message") - message([message]: ArgsOf<"message">) { + @On("messageCreate") + message([message]: ArgsOf<"messageCreate">) { //... } @@ -65,7 +69,7 @@ abstract class AppDiscord { When can setup some guards globaly by assigning `Client.guards` -> The global guards are set statically, you can access it by `Client.guards` +> The global guards are set statically, you can access it by `Client.guards` > > Global guards are executed before @Discord guards @@ -75,6 +79,7 @@ import { Client } from "@typeit/discord"; async function start() { const client = new Client({ + botId: "test", classes: [ `${__dirname}/*Discord.ts`, // glob string to load the classes `${__dirname}/*Discord.js`, // If you compile using "tsc" the file extension change to .js @@ -95,7 +100,7 @@ start(); ## The guard functions -Here is a simple example of a guard function (the payload and the client instance are injected like for events) +Here is a simple example of a guard function (the payload and the client instance are injected like for events) Guards work like `Koa`'s, it's a function passed in parameter (third parameter in the guard function) and you will have to call if the guard is passed. @@ -104,7 +109,11 @@ Guards work like `Koa`'s, it's a function passed in parameter (third parameter i ```typescript import { GuardFunction, ArgsOf } from "@typeit/discord"; -export const NotBot: GuardFunction> = ([message], client, next) => { +export const NotBot: GuardFunction> = ( + [message], + client, + next +) => { if (client.user.id !== message.author.id) { await next(); } @@ -117,7 +126,11 @@ If you have to indicate parameters for a guard function you can simple use the " import { GuardFunction } from "@typeit/discord"; export function Prefix(text: string, replace: boolean = true) { - const guard: GuardFunction> = ([message], client, next) => { + const guard: GuardFunction> = ( + [message], + client, + next + ) => { const startWith = message.content.startsWith(text); if (replace) { message.content = message.content.replace(text, ""); @@ -138,7 +151,7 @@ As 4th parameter you receive a basic empty object that can be used to transmit d ```typescript import { GuardFunction } from "@typeit/discord"; -export const NotBot: GuardFunction> = ( +export const NotBot: GuardFunction> = ( [message], client, next, @@ -161,7 +174,11 @@ import { Prefix } from "./Prefix"; abstract class AppDiscord { @Slash() @Guard(NotBot, Prefix("!")) - async hello(interaction: CommandInteraction, client: Client, guardDatas: any) { + async hello( + interaction: CommandInteraction, + client: Client, + guardDatas: any + ) { console.log(guardDatas.message); // > the NotBot guard passed } diff --git a/docs/src/decorators/guild.md b/docs/src/decorators/guild.md index 28e7ff06..f462b73e 100644 --- a/docs/src/decorators/guild.md +++ b/docs/src/decorators/guild.md @@ -1,48 +1,49 @@ # @Guild -You can specify in wich guilds your @Slash commands are created by decorating the method with @Slash and @Guild + +You can specify in which guilds your @Slash commands are created by decorating the method with @Slash and @Guild ```ts @Discord() abstract class AppDiscord { @Guild("GUILD_ID") // Only created on the guild GUILD_ID @Slash("hello") - private hello( - ) { + private hello() { // ... } @Guild("GUILD_ID", "GUILD_ID2") // Only created on the guild GUILD_ID and GUILD_ID2 @Slash("bye") - private bye( - ) { + private bye() { // ... } } ``` ## Guild at class level + You can set the guild IDs for all @Slash inside the class by decorating the class with @Guild + ```ts @Discord() @Guild("GUILD_ID", "GUILD_ID2") class DiscordBot { @Slash("hello") // Only created on the guild GUILD_ID and GUILD_ID2 - private hello( - ) { + private hello() { // ... } @Slash("hello2") // Only created on the guild GUILD_ID and GUILD_ID2 - private hello2( - ) { + private hello2() { // ... } } ``` ## Params + `@Guild(...guildIDs: string[])` ### roleIDs + `string[]` The guilds IDs list diff --git a/docs/src/decorators/on.md b/docs/src/decorators/on.md index a2712e64..7ab2a3bf 100644 --- a/docs/src/decorators/on.md +++ b/docs/src/decorators/on.md @@ -1,7 +1,8 @@ # @On - Discord events -We can declare methods that will be executed whenever a Discord event is triggered. -Our methods must be decorated with the `@On(event: string)` or [@Once(event: string)](/decorators/once) decorator. +We can declare methods that will be executed whenever a Discord event is triggered. + +Our methods must be decorated with the `@On(event: string)` or [@Once(event: string)](/decorators/once) decorator. That's simple, when the event is triggered, the method is called: @@ -10,7 +11,7 @@ import { Discord, On, Once } from "@typeit/discord"; @Discord() abstract class AppDiscord { - @On("message") + @On("messageCreate") private onMessage() { // ... } @@ -23,9 +24,11 @@ abstract class AppDiscord { ``` ## Get the event payload + For each event a list of arguments is injected in your decorated method, you can type this list thanks to the `ArgsOf<"YOUR_EVENT">` type provided by discord.**ts**. You also receive other useful arguments after that: + 1. The event payload (`ArgsOf<"YOUR_EVENT">`) 2. The `Client` instance 3. The [guards](/decorators/guards/) payload @@ -33,18 +36,13 @@ You also receive other useful arguments after that: > You should use JS desctructuring for `ArgsOf<"YOUR_EVENT">` like in this example ```typescript -import { - Discord, - On, - Client, - ArgsOf -} from "@typeit/discord"; +import { Discord, On, Client, ArgsOf } from "@typeit/discord"; @Discord() abstract class AppDiscord { - @On("message") + @On("messageCreate") private onMessage( - [message]: ArgsOf<"message">, // Type message automatically + [message]: ArgsOf<"messageCreate">, // Type message automatically client: Client, // Client instance injected here, guardPayload: any ) { diff --git a/docs/src/decorators/once.md b/docs/src/decorators/once.md index 96337c96..23eef009 100644 --- a/docs/src/decorators/once.md +++ b/docs/src/decorators/once.md @@ -1,4 +1,5 @@ # @Once - Discord events + It's exactly the same behavior as [@On](/decorators/on) but the method is only executed once ```typescript @@ -14,9 +15,11 @@ abstract class AppDiscord { ``` ## Get the event payload + For each event a list of arguments is injected in your decorated method, you can type this list thanks to the `ArgsOf<"YOUR_EVENT">` type provided by discord.**ts**. You also receive other useful arguments after that: + 1. The event payload (`ArgsOf<"YOUR_EVENT">`) 2. The `Client` instance 3. The [guards](/decorators/guards/) payload @@ -24,22 +27,17 @@ You also receive other useful arguments after that: > You should use JS desctructuring for `ArgsOf<"YOUR_EVENT">` like in this example ```typescript -import { - Discord, - On, - Client, - ArgsOf -} from "@typeit/discord"; +import { Discord, On, Client, ArgsOf } from "@typeit/discord"; @Discord() abstract class AppDiscord { - @On("message") + @On("messageCreate") private onMessage( - [message]: ArgsOf<"message">, // Type message automatically + [message]: ArgsOf<"messageCreate">, // Type message automatically client: Client, // Client instance injected here, guardPayload: any ) { // ... } } -``` \ No newline at end of file +``` diff --git a/docs/src/decorators/option.md b/docs/src/decorators/option.md index 66647101..4b9d6d74 100644 --- a/docs/src/decorators/option.md +++ b/docs/src/decorators/option.md @@ -1,4 +1,5 @@ # Option + A Slash Command can have multiple options (parameters) > query is an option in this image @@ -6,7 +7,9 @@ A Slash Command can have multiple options (parameters) ![](/discord.ts/options.png) ## Declare an option + To declare an option you simply use the `@Option` decorator before a method parameter + ```ts @Discord() class DiscordBot { @@ -25,6 +28,7 @@ class DiscordBot { ``` ## Automatic typing + An option infer the type from TypeScript in this example, discord.**ts** knows that your options are both `number` because you typed the parameters discord.**ts** convert automatically the infered type into discord.**js** options types @@ -47,9 +51,11 @@ class DiscordBot { ``` ## Manual typing + If you want to specify the type manually you can do it: + ```ts -import { TextChannel, VoiceChannel, CommandInteraction } from "discord.js" +import { TextChannel, VoiceChannel, CommandInteraction } from "discord.js"; @Discord() class DiscordBot { @@ -66,8 +72,10 @@ class DiscordBot { ``` ## Type inferance + - `"STRING"` - **Infered from `String`** + **Infered from `String`** + ```ts fn( @Option("x") @@ -76,7 +84,8 @@ class DiscordBot { ``` - `"BOOLEAN"` - **Infered from `Boolean`** + **Infered from `Boolean`** + ```ts fn( @Option("x") @@ -85,7 +94,8 @@ class DiscordBot { ``` - `"INTEGER"` - **Infered from `Number`** + **Infered from `Number`** + ```ts fn( @Option("x") @@ -94,7 +104,8 @@ class DiscordBot { ``` - `"ROLE"` - **Infered from `Role`** + **Infered from `Role`** + ```ts fn( @Option("x") @@ -103,7 +114,8 @@ class DiscordBot { ``` - `"USER"` - **Infered from `User` (or `ClientUser`, not recommended)** + **Infered from `User` (or `ClientUser`, not recommended)** + ```ts fn( @Option("x") @@ -112,7 +124,8 @@ class DiscordBot { ``` - `"CHANNEL"` - **Infered from `Channel` (or `TextChannel` / `VoiceChannel`, not recommended)** + **Infered from `Channel` (or `TextChannel` / `VoiceChannel`, not recommended)** + ```ts fn( @Option("x") @@ -120,7 +133,8 @@ class DiscordBot { ``` - `"MENTIONABLE"` - **No inferance, use:** + **No inferance, use:** + ```ts fn( @Option("x", "MENTIONABLE") @@ -130,11 +144,11 @@ class DiscordBot { - `"SUB_COMMAND"` No inferance, use [@Group](/decorators/group/) - - `"SUB_COMMAND_GROUP"` No inferance, use [@Group](/decorators/group/) ## Signature + ```ts Option(name: string); Option(name: string, type: OptionValueType | OptionType); @@ -143,44 +157,49 @@ Option(name: string, type: OptionValueType | OptionType, params: OptionParams); ``` ## Params + The parameters of an @Option is an object as the last parameter ### Description + `string` -`OPTION_NAME - OPTION_TYPE` by default +`OPTION_NAME - OPTION_TYPE` by default You can set the description of the option ### Required + `bool` -`false` by default +`false` by default The option is required or not ## Set the default required value + if you want to set the default required value, you can use `client.requiredByDefault` ```ts const client = new Client({ - intents: [ - Intents.FLAGS.GUILDS, - Intents.FLAGS.GUILD_MESSAGES, - ], + botId: "test", + intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES], classes: [ `${__dirname}/*Discord.ts`, // glob string to load the classes `${__dirname}/*Discord.js`, // If you compile using "tsc" the file extension change to .js ], silent: false, - requiredByDefault: true + requiredByDefault: true, }); ``` ## Autocompletion (Option's choices) + You can use the [@Choice](/decorators/choice/) decorator ## Option order + **You have to put required options before optional ones** -Or you will get this error: +Or you will get this error: + ``` (node:64399) UnhandledPromiseRejectionWarning: DiscordAPIError: Invalid Form Body options[1]: Required options must be placed before non-required options diff --git a/docs/src/decorators/permission.md b/docs/src/decorators/permission.md index bbaa6feb..01c58918 100644 --- a/docs/src/decorators/permission.md +++ b/docs/src/decorators/permission.md @@ -1,4 +1,5 @@ # @Permission + You can set some permissions to your Slash commands The permissions are based on a **role id** or **user id** that you specify on the @Permission decorator @@ -13,7 +14,8 @@ Permissions are only available for Guild specific Slash commands > You can manage it by yourself using your own the Slashes `Client` API and creating your own `client.initSlashes()` implementation ## Setup permissions -You just decorate your parameter with one or multiple @Permission ! + +You just decorate your parameter with one or multiple @Permission ! ```ts @Discord() @@ -21,41 +23,43 @@ class DiscordBot { @Permission("USER_ID", "USER") // Only the role that has this USER_ID can use this command @Permission("ROLE_ID", "ROLE") // Only the role that has this ROLE_ID can use this command @Slash("hello") - private hello( - ) { + private hello() { // ... } } ``` ## Permissions at class level + You can set the permissions for all @Slash inside the class by decorating the class with @Permission + ```ts @Discord() @Permission("USER_ID", "USER") // Only the role that has this USER_ID can use this command @Permission("ROLE_ID", "ROLE") // Only the role that has this ROLE_ID can use this command class DiscordBot { @Slash("hello") // Only the role that has this ROLE_ID can use this command - private hello( - ) { + private hello() { // ... } @Slash("hello2") // Only the role that has this ROLE_ID can use this command - private hello2( - ) { + private hello2() { // ... } } ``` ## Params + `@Permission(id: string, type: "USER" | "ROLE")` ### id + `string` The id if the user or role ### type + `"ROLE" | "USER"` It specify if the permission is given to a user or a role diff --git a/docs/src/decorators/selectmenu.md b/docs/src/decorators/selectmenu.md new file mode 100644 index 00000000..2edc18ca --- /dev/null +++ b/docs/src/decorators/selectmenu.md @@ -0,0 +1,5 @@ +# @SelectMenu + +::: danger +doc for `@SelectMenu` is not ready +::: diff --git a/docs/src/decorators/slash.md b/docs/src/decorators/slash.md index de4484a8..0096dab2 100644 --- a/docs/src/decorators/slash.md +++ b/docs/src/decorators/slash.md @@ -1,4 +1,5 @@ # @Slash - Discord commands + Discord has it's own command system now, you can simply declare commands and use Slash commands this way ```ts @@ -7,8 +8,7 @@ import { Discord, Slash } from "@typeit/discord"; @Discord() abstract class AppDiscord { @Slash("hello") - private hello( - ) { + private hello() { // ... } } @@ -18,8 +18,9 @@ abstract class AppDiscord { It require a bit of configuration at you Client initialization. You have to manualy execute and initialize your Slash commands by using: + - `client.initSlashes()` -- `client.executeSlash(interaction)` +- `client.executeInteraction(interaction)` This provide flexibility in your code @@ -28,18 +29,16 @@ import { Client } from "@typeit/discord"; async function start() { const client = new Client({ - intents: [ - Intents.FLAGS.GUILDS, - Intents.FLAGS.GUILD_MESSAGES, - ], + botId: "test", + intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES], }); client.once("ready", async () => { await client.initSlashes(); }); - client.on("interaction", (interaction) => { - client.executeSlash(interaction); + client.on("interactionCreate", (interaction) => { + client.executeInteraction(interaction); }); await client.login("YOUR_TOKEN"); @@ -53,19 +52,20 @@ start(); ```ts const client = new Client({ - intents: [ - Intents.FLAGS.GUILDS, - Intents.FLAGS.GUILD_MESSAGES, - ], - slashGuilds: process.DEV ? ["GUILD_ID"] : undefined + botId: "test", + intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES], + slashGuilds: process.DEV ? ["GUILD_ID"] : undefined, }); ``` + ::: ## Slash API + By using the Client class you can access and manage to Slashes ### Clear slashes from Discord cache + You can remove Slash commands from the Discord cache by using `client.clearSlashes(...guildIDs: string[])` > If you do not specify the guild id you operate on global Slash commands @@ -79,7 +79,9 @@ client.once("ready", async () => { ``` ### Fetch slashes from Discord + or fetch them by using `client.fetchSlashes(guildID: string)` + > If you do not specify the guild id you operate on global Slash commands ```ts @@ -90,12 +92,15 @@ client.once("ready", async () => { ``` ### Get declared slashes + You can retrieve the list of declared Slashes on your application (declared using @Slash) + ```ts const slashes = client.slashes; ``` ### Apply Slash to specific guild globaly + Instead on doing this for all of your @Slash: > You can manage it by yourself using your own the Slashes `Client` API and creating your own `client.initSlashes()` implementation @@ -105,15 +110,13 @@ Instead on doing this for all of your @Slash: abstract class AppDiscord { @Guild("GUILD_ID") @Slash("hello") - private hello( - ) { + private hello() { // ... } @Guild("GUILD_ID") @Slash("bye") - private bye( - ) { + private bye() { // ... } } @@ -123,25 +126,22 @@ You can do: ```ts const client = new Client({ - intents: [ - Intents.FLAGS.GUILDS, - Intents.FLAGS.GUILD_MESSAGES, - ], - slashGuilds: ["GUILD_ID"] + botId: "test", + intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES], + slashGuilds: ["GUILD_ID"], }); ``` + ```ts @Discord() abstract class AppDiscord { @Slash("hello") // Applied on GUILD_ID - private hello( - ) { + private hello() { // ... } @Slash("bye") // Applied on GUILD_ID - private bye( - ) { + private bye() { // ... } } @@ -150,28 +150,34 @@ abstract class AppDiscord { ## Params ### Name + `string` The Slash command name ### Description + `string` The Slash command description ### Guilds -`string[]` + +`string[]` The guilds where the command is created ### defaultPermission + `boolean` -`true` by default +`true` by default "You can also set a default_permission on your commands if you want them to be disabled by default when your app is added to a new guild. Setting default_permission to false will disallow anyone in a guild from using the command--even Administrators and guild owners--unless a specific overwrite is configured. It will also disable the command from being usable in DMs." ## Authorize your bot to use Slash commands + On the Discord's developer portal, select your bot, go to the OAuth2 tab and check the box **bot** AND **applications.commands** ![](/discord.ts/authorize1.png) ![](/discord.ts/authorize2.png) ## See also + - [discord.js's documentation with Interactions (Slash commands)](https://discord.js.org/#/docs/main/master/general/welcome) - [Discord's Slash commands interactions](https://discord.com/developers/docs/interactions/slash-commands) diff --git a/docs/src/general/argsof.md b/docs/src/general/argsof.md index fe163c3a..83fda299 100644 --- a/docs/src/general/argsof.md +++ b/docs/src/general/argsof.md @@ -1,22 +1,18 @@ -# ArgsOf +# ArgsOf + `ArgsOf` type your events payload as an array, just pass an event (as string) in the type parameter and it types your array with the related event's parameters You can get the list of the events and the payload type in the ["List of events" section](/general/events/) ```ts -import { - Discord, - On, - Client, - ArgsOf -} from "@typeit/discord"; +import { Discord, On, Client, ArgsOf } from "@typeit/discord"; @Discord() abstract class AppDiscord { - @On("message") + @On("messageCreate") private onMessage( // The type of message is Message - [message]: ArgsOf<"message"> + [message]: ArgsOf<"messageCreate"> ) { // ... } @@ -24,7 +20,7 @@ abstract class AppDiscord { @On("channelUpdate") private onMessage( // The type of channel1 and channel2 is TextChannel - [channel1, channel2]: ArgsOf<"channelUpdate">, + [channel1, channel2]: ArgsOf<"channelUpdate"> ) { // ... } diff --git a/docs/src/general/client.md b/docs/src/general/client.md index f7ff8cfa..1abf60b4 100644 --- a/docs/src/general/client.md +++ b/docs/src/general/client.md @@ -1,28 +1,42 @@ # Client + It manage all the operations between your app, Discord's API and discord.js ## Setup and start your application + In order to start your application, you must use the discord.**ts**'s Client (not the client that is provided by discord.**js**!). It works the same as the discord.**js**'s Client (same methods, properties, ...). -- **`classes` (required)** - `string[]` +- **`intents` (required)** + `Intents[]` + [see Intents](#intents) + +- **`botId`** + `string` (`bot` by default) + a bot id, help you manage your bot interactions, events (this is important in case there are more than one bot running in single instance) + +- **`prefix`** + `string | ((message: Message) => Promise)` (`!` by default) + simple commands use use this prefix by default, use function to fetch different prefix for different guilds + +- **`classes`** + `string[]` Indicate the class jacket of your classes containing the `@Discord` decorator. It accepts a list of classes or of (glob) paths -- **`silent`** - `boolean` (`false` by default) +- **`silent`** + `boolean` (`true` by default) Allows you to disable your event information at startup -- **`requiredByDefault`** +- **`requiredByDefault`** `boolean` (`false` by default) - The `@Option` are required by default + The `@Option` are required by default -- **`guards`** +- **`guards`** `GuardFunction[]` Global guards, it's an array of functions -- **`slashGuilds`** - `string[]` +- **`slashGuilds`** + `string[]` The slash commands are executed only on this list of guilds by default **You must specify the glob path(s) where your decorated classes are** @@ -35,10 +49,8 @@ import { Client } from "@typeit/discord"; async function start() { const client = new Client({ - intents: [ - Intents.FLAGS.GUILDS, - Intents.FLAGS.GUILD_MESSAGES, - ], + botId: "test", + intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES], classes: [ `${__dirname}/*Discord.ts`, // glob string to load the classes `${__dirname}/*Discord.js`, // If you compile using "tsc" the file extension change to .js @@ -53,34 +65,36 @@ start(); ``` ## Intents -You must specify the "**intents**" of your bot when you initialize the Client, it specify wich informations your bot receive from the Discord's servers, **it's different from the permissions** -*Maintaining a stateful application can be difficult when it comes to the amount of data you're expected to process, especially at scale. Gateway Intents are a system to help you lower that computational burden.* +You must specify the "**intents**" of your bot when you initialize the Client, it specify which informations your bot receive from the Discord's servers, **it's different from the permissions** + +_Maintaining a stateful application can be difficult when it comes to the amount of data you're expected to process, especially at scale. Gateway Intents are a system to help you lower that computational burden._ -*When identifying to the gateway, you can specify an intents parameter which allows you to conditionally subscribe to pre-defined "intents", groups of events defined by Discord. If you do not specify a certain intent, you will not receive any of the gateway events that are batched into that group.* +_When identifying to the gateway, you can specify an intents parameter which allows you to conditionally subscribe to pre-defined "intents", groups of events defined by Discord. If you do not specify a certain intent, you will not receive any of the gateway events that are batched into that group._ ::: danger If an event of your app isn't triggered, you probably missed an Intent ::: ### Basic intents, just text messages + ```ts import { Intents } from "discord.js"; const client = new Client({ - intents: [ - Intents.FLAGS.GUILDS, - Intents.FLAGS.GUILD_MESSAGES, - ], + botId: "test", + intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES], // ... }); ``` ### Voice activity intent, the ability to speak + ```ts import { Intents } from "discord.js"; const client = new Client({ + botId: "test", intents: [ Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES, @@ -91,9 +105,11 @@ const client = new Client({ ``` ### List of all the intents -[You can find the complete list here](https://discord.com/developers/docs/topics/gateway#list-of-intents) -**Most used ones** +[You can find the complete list here](https://discord.com/developers/docs/topics/gateway#list-of-intents) + +**Most used ones** + - GUILDS - GUILD_MEMBERS - GUILD_BANS @@ -108,12 +124,13 @@ const client = new Client({ - GUILD_MESSAGE_TYPING - DIRECT_MESSAGES - DIRECT_MESSAGE_REACTIONS -- DIRECT_MESSAGE_TYPING +- DIRECT_MESSAGE_TYPING ```ts import { Intents } from "discord.js"; const client = new Client({ + botId: "test", intents: [ Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES, @@ -134,8 +151,10 @@ const client = new Client({ ``` ## Slashes API + It also implements an [API for your @Slash](/decorators/slash.html#slash-api) ## See also + - [discord.js documentation](https://discord.js.org/#/docs/main/stable/class/Intents) - [Discord's documentation](https://discord.com/developers/docs/topics/gateway#list-of-intents) diff --git a/docs/src/general/debugging.md b/docs/src/general/debugging.md index 55ba60e6..83e3c924 100644 --- a/docs/src/general/debugging.md +++ b/docs/src/general/debugging.md @@ -1,33 +1,33 @@ # Use the VSCode debugger to debug your bot 1. Create the `.vscode/launch.json` file at your project root directory if the file do not already exists - 2. Install ts-node as a dev dependency + ``` npm i -D ts-node ``` 3. Copy paste this into your `launch.json` file + ```json - { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Debug bot", - "protocol": "inspector", - "args": ["${workspaceRoot}/PATH_TO_YOUR_MAIN.ts"], - "cwd": "${workspaceRoot}", - "runtimeArgs": ["-r", "ts-node/register/transpile-only"], - "internalConsoleOptions": "neverOpen" - } - ] - } + { + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug bot", + "protocol": "inspector", + "args": ["${workspaceRoot}/PATH_TO_YOUR_MAIN.ts"], + "cwd": "${workspaceRoot}", + "runtimeArgs": ["-r", "ts-node/register/transpile-only"], + "internalConsoleOptions": "neverOpen" + } + ] + } ``` - -4. You can now put some breakpoints, go to the debug tab in VSCode and launch your bot +4. You can now put some breakpoints, go to the debug tab in VSCode and launch your bot diff --git a/docs/src/general/events.md b/docs/src/general/events.md index ec2de92a..08cc96e9 100644 --- a/docs/src/general/events.md +++ b/docs/src/general/events.md @@ -1,9 +1,9 @@ # List of the discord.js events + Here is all the `DiscordEvents` and their parameters (`discord.js`) - **channelCreate** `[Channel]` - - **channelDelete** `[Channel | PartialDMChannel]` @@ -15,10 +15,8 @@ Here is all the `DiscordEvents` and their parameters (`discord.js`) - **debug** `[string]` - - **warn** `[string]` - - **disconnect** `[any, number]` diff --git a/docs/src/general/metadatastorage.md b/docs/src/general/metadatastorage.md index dac722c7..d4e6d022 100644 --- a/docs/src/general/metadatastorage.md +++ b/docs/src/general/metadatastorage.md @@ -1,11 +1,14 @@ # MetadataStorage + The MetadataStorage store all the informations about your decorators, you can get the informations related to them by using `MetadataStorage.instance` ```ts -import { MetadataStorage } from "@typeit/discord.ts" +import { MetadataStorage } from "@typeit/discord.ts"; MetadataStorage.instance.slashes; MetadataStorage.instance.events; MetadataStorage.instance.discords; +MetadataStorage.instance.buttons; +MetadataStorage.instance.selectMenus; // ... ``` diff --git a/docs/src/general/sharding.md b/docs/src/general/sharding.md index 7a50a213..99c23b1b 100644 --- a/docs/src/general/sharding.md +++ b/docs/src/general/sharding.md @@ -6,9 +6,9 @@ Sharding your bot with `@typeit/discord`. ## Purpose -Sharding is the process of splitting your main discord process into multiple shards to help with the load when your bot is in 2,500+ guilds. discord.**js** has recommended to start making updates for sharding at around 2,000 guilds. +Sharding is the process of splitting your main discord process into multiple shards to help with the load when your bot is in 2,500+ guilds. discord.**js** has recommended to start making updates for sharding at around 2,000 guilds. -[Discord.js Sharding Guide](https://discordjs.guide/sharding/#when-to-shard) +[Discord.js Sharding Guide](https://discordjs.guide/sharding/#when-to-shard) When you hit that milestone and need to begin the sharding process this guide will serve as a starting document to help you get set up. @@ -19,9 +19,9 @@ When you hit that milestone and need to begin the sharding process this guide wi ### What if my bot is in less than 2,000 servers? -discord.**js** has stated +discord.**js** has stated -"*Sharding is only necessary at 2,500 guildsβ€”at that point, Discord will not allow your bot to login without sharding. With that in mind, you should consider this when your bot is around 2,000 guilds, which should be enough time to get this working. Contrary to popular belief, sharding itself is very simple. It can be complex depending on your bot's needs, however. If your bot is in a total of 2,000 or more servers, then please continue with this guide. Otherwise, it may be a good idea to wait until then.*" +"_Sharding is only necessary at 2,500 guildsβ€”at that point, Discord will not allow your bot to login without sharding. With that in mind, you should consider this when your bot is around 2,000 guilds, which should be enough time to get this working. Contrary to popular belief, sharding itself is very simple. It can be complex depending on your bot's needs, however. If your bot is in a total of 2,000 or more servers, then please continue with this guide. Otherwise, it may be a good idea to wait until then._" However if you are curious you may continue to read this doc! But don't worry about sharding until 2,000 guilds. Focus on building a quality bot as sharding adds more complexity. @@ -44,12 +44,11 @@ I found success with using this `tsconfig.json` "forceConsistentCasingInFileNames": true, "lib": ["es2020", "esnext.asynciterable"], "moduleResolution": "node", - "outDir": "./src/build", + "outDir": "./src/build" }, "exclude": ["node_modules"], "indent": [true, "spaces", 2] } - ``` If you are receiving errors that complain about imports. Try using the following import where the compiler complains about the import. @@ -94,17 +93,14 @@ Read the [discord.js sharding docs](https://discordjs.guide/sharding/). You will make a new class in the `shard.bot.ts` file. I have named my class ShardBot ```typescript -export class ShardBot { - -} +export class ShardBot {} ``` Inside this class I have defined a `static start` method that gets called outside of the ShardBot class. ```typescript export class ShardBot { - static start(): void { - } + static start(): void {} } ShardBot.start(); @@ -118,7 +114,6 @@ import { environment } from "../../environments/environment"; export class ShardBot { static start(): void { - const manager = new ShardingManager("./src/build/src/app/entry.bot.js", { token: environment.DISCORD_TOKEN, }); @@ -126,7 +121,7 @@ export class ShardBot { manager.on("shardCreate", (shard) => { console.log(`Launched shard ${shard.id}`); }); - + manager.spawn(); } } @@ -143,7 +138,7 @@ Now that your bot compiles and has the shard file we can run the bot with the sh `node build/app/shard.bot.js` -will start the shard here. +will start the shard here. ::: warning Make sure you provide the correct path to the shard file when running with node. diff --git a/docs/src/index.md b/docs/src/index.md index b9c5a8d8..ffa90601 100755 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -6,11 +6,11 @@ title: discord.ts actionText: Quick Start β†’ actionLink: /installation/ features: -- title: Decorators - details: Create your bot using Typescript decorators ! -- title: Slash commands - details: Implement a Discord's Slash commands system simply ! -- title: discord.js support - details: You can use discord.js along discord.ts without any problems ! + - title: Decorators + details: Create your bot using Typescript decorators ! + - title: Slash commands + details: Implement a Discord's Slash commands system simply ! + - title: discord.js support + details: You can use discord.js along discord.ts without any problems ! footer: Made by discord.ts team with ❀️ --- diff --git a/docs/src/installation/README.md b/docs/src/installation/README.md index e8de0c95..7996453e 100644 --- a/docs/src/installation/README.md +++ b/docs/src/installation/README.md @@ -24,14 +24,15 @@ This module is an extension of **[discord.**js**](https://discordjs.guide/)**, s This library allows you to use TypeScript decorators on discord.**js**, it simplify your code and improve the readability ! ## Easy setup - starter project + 1. Clone this project `git clone https://github.com/owencalvin/discord.js-template` 2. Run `npm i` - 3. And let's go, everything was done for you! πŸš€ ## Installation + Use [npm](https://www.npmjs.com/package/@typeit/discord) or yarn to install **@typeit/discord** with **discord.js** > You use the npm @slash tag to install version of discord.ts **@typeit/discord** that includes Slash commands (this version) @@ -39,11 +40,13 @@ Use [npm](https://www.npmjs.com/package/@typeit/discord) or yarn to install **@t ::: danger For the moment discord.**js** didn't release the v13 on npm, you have to install it this way (You also have to install "reflect-metadata" for the decorators) + ```sh -npm i @typeit/discord@slash reflect-metadata https://github.com/discordjs/discord.js +npm i @typeit/discord@slash reflect-metadata https://github.com/discordjs/discord.js ``` Install your TypeScript dev dependencies too + ```sh npm i -D @types/node typescript tslib ``` @@ -56,7 +59,7 @@ And you should see this in your package.json "dependencies": { "@typeit/discord": "^X.X.X", "discord.js": "github:discordjs/discord.js", - "reflect-metadata": "^0.1.13", + "reflect-metadata": "^0.1.13" }, "devDependencies": { "@types/node": "^15.0.3", @@ -66,16 +69,19 @@ And you should see this in your package.json // ... } ``` + ::: ## Execution environnement -To start your bot you can compile your code into JavaScript with TypeScript using the `tsc` command or simple use [ts-node](https://www.npmjs.com/package/ts-node). + +To start your bot you can compile your code into JavaScript with TypeScript using the `tsc` command or simple use [ts-node](https://www.npmjs.com/package/ts-node). ::: danger Be aware that if you compile your code into JavaScript with `tsc` you have to specify .js files when you instanciate your Client ```ts const client = new Client({ + botId: "test", // glob string to load the classes classes: [ `${__dirname}/*Discord.ts`, // If you use ts-node @@ -86,9 +92,11 @@ const client = new Client({ guards: [NotBot, Prefix("!")], }); ``` + ::: ## tsconfig.json + Your tsconfig.json file should look like this: ```json @@ -111,7 +119,9 @@ Your tsconfig.json file should look like this: ``` ## reflect-metadata + You have to import the reflect-metadata module on your main file for the decorators (for the reflection) + ```ts import "reflect-metadata"; import { Client } from "@typeit/discord"; @@ -124,14 +134,16 @@ start(); ``` ## Need help? -**[Simply join the Discord server](https://discord.gg/VDjwu8E)** + +**[Simply join the Discord server](https://discord.gg/VDjwu8E)** You can also find help with the [different projects that use discord.ts](https://github.com/OwenCalvin/discord.ts/network/dependents?package_id=UGFja2FnZS00Njc1MzYwNzU%3D) and in the [examples folder](https://github.com/OwenCalvin/discord.ts/tree/master/examples) ## See also + - [discord.js's documentation with Interactions (Slash commands)](https://discord.js.org/#/docs/main/master/general/welcome) - [Discord's Slash commands interactions](https://discord.com/developers/docs/interactions/slash-commands) ## Next step -[Setup and start your application πŸš€](/general/client/) +[Setup and start your application πŸš€](/general/client/) diff --git a/examples/button/Main.ts b/examples/button/Main.ts new file mode 100644 index 00000000..6aaeed38 --- /dev/null +++ b/examples/button/Main.ts @@ -0,0 +1,39 @@ +import "reflect-metadata"; +import { Client } from "../../src"; +import { Intents } from "discord.js"; + +export class Main { + private static _client: Client; + + static get Client(): Client { + return this._client; + } + + static async start() { + this._client = new Client({ + classes: [ + `${__dirname}/discords/*.ts`, // glob string to load the classes + `${__dirname}/discords/*.js`, // If you compile your bot, the file extension will be .js + ], + intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES], + // slashGuilds: [YOUR_GUILD_ID], + requiredByDefault: true, + }); + + // In the login method, you must specify the glob string to load your classes (for the framework). + // In this case that's not necessary because the entry point of your application is this file. + await this._client.login("YOU_TOKEN"); + + this._client.once("ready", async () => { + await this._client.initSlashes(); + + console.log("Bot started"); + }); + + this._client.on("interactionCreate", (interaction) => { + this._client.executeInteraction(interaction); + }); + } +} + +Main.start(); diff --git a/examples/button/discords/AppDiscord.ts b/examples/button/discords/AppDiscord.ts new file mode 100644 index 00000000..7d6037c2 --- /dev/null +++ b/examples/button/discords/AppDiscord.ts @@ -0,0 +1,209 @@ +import { + CommandInteraction, + MessageButton, + MessageActionRow, + ButtonInteraction, + EmojiIdentifierResolvable, +} from "discord.js"; +import { + Discord, + Slash, + Button, + Option, + Description, + Choices, +} from "../../../src"; +import { randomInt } from "crypto"; + +enum spcChoice { + Stone = "Stone", + Paper = "Paper", + Scissor = "Scissor", +} + +enum spcResult { + WIN, + LOSS, + DRAW, +} + +class spcProposition { + public static propositions = [ + new spcProposition(spcChoice.Stone, "πŸ’Ž", "spc-stone"), + new spcProposition(spcChoice.Paper, "🧻", "spc-paper"), + new spcProposition(spcChoice.Scissor, "βœ‚οΈ", "spc-scissor"), + ]; + + public choice: spcChoice; + public emoji: EmojiIdentifierResolvable; + public buttonCustomID: "spc-stone" | "spc-paper" | "spc-scissor"; + + constructor( + choice: spcChoice, + emoji: EmojiIdentifierResolvable, + buttonCustomID: "spc-stone" | "spc-paper" | "spc-scissor" + ) { + this.choice = choice; + this.emoji = emoji; + this.buttonCustomID = buttonCustomID; + } + + public static nameToClass(choice: string) { + return this.propositions.find( + (proposition) => choice === proposition.choice + ); + } + + public static buttonCustomIDToClass(buttonCustomID: string) { + return this.propositions.find( + (proposition) => buttonCustomID === proposition.buttonCustomID + ); + } +} + +@Discord() +export abstract class StonePaperScissor { + @Slash("stonepaperscissor") + @Description( + "What could be more fun than play Rock Paper Scissor with a bot?" + ) + private async spc( + @Choices(spcChoice) + @Option("Choice", { + description: + "Your choose. If empty, it will send a message with buttons to choose and play instead.", + required: false, + }) + choice: spcChoice, + interaction: CommandInteraction + ) { + await interaction.defer(); + + if (choice) { + const playerChoice = spcProposition.nameToClass(choice); + const botChoice = StonePaperScissor.spcPlayBot(); + const result = StonePaperScissor.isWinSpc(playerChoice, botChoice); + + interaction.editReply( + StonePaperScissor.spcResultProcess(playerChoice, botChoice, result) + ); + } else { + const buttonStone = new MessageButton() + .setLabel("Stone") + .setEmoji("πŸ’Ž") + .setStyle("PRIMARY") + .setCustomId("spc-stone"); + + const buttonPaper = new MessageButton() + .setLabel("Paper") + .setEmoji("🧻") + .setStyle("PRIMARY") + .setCustomId("spc-paper"); + + const buttonScissor = new MessageButton() + .setLabel("Scissor") + .setEmoji("βœ‚οΈ") + .setStyle("PRIMARY") + .setCustomId("spc-scissor"); + + const buttonWell = new MessageButton() + .setLabel("Well") + .setEmoji("❓") + .setStyle("DANGER") + .setCustomId("spc-well") + .setDisabled(true); + + const buttonRow = new MessageActionRow().addComponents( + buttonStone, + buttonPaper, + buttonScissor, + buttonWell + ); + + interaction.editReply({ + content: "Ok let's go. 1v1 Stone Paper Scissor. Go choose!", + components: [buttonRow], + }); + + setTimeout( + (interaction) => interaction.deleteReply(), + 10 * 60 * 1000, + interaction + ); + } + } + + @Button("spc-stone") + @Button("spc-paper") + @Button("spc-scissor") + private async spcButton(interaction: ButtonInteraction) { + await interaction.defer(); + + const playerChoice = spcProposition.buttonCustomIDToClass( + interaction.customId + ); + const botChoice = StonePaperScissor.spcPlayBot(); + const result = StonePaperScissor.isWinSpc(playerChoice, botChoice); + + interaction.editReply( + StonePaperScissor.spcResultProcess(playerChoice, botChoice, result) + ); + + setTimeout( + (interaction) => { + try { + interaction.deleteReply(); + } catch (err) { + console.error(err); + } + }, + 30000, + interaction + ); + } + + private static isWinSpc( + player: spcProposition, + bot: spcProposition + ): spcResult { + switch (player.choice) { + case spcChoice.Stone: + if (bot.choice === spcChoice.Scissor) return spcResult.WIN; + if (bot.choice === spcChoice.Paper) return spcResult.LOSS; + return spcResult.DRAW; + case spcChoice.Paper: + if (bot.choice === spcChoice.Stone) return spcResult.WIN; + if (bot.choice === spcChoice.Scissor) return spcResult.LOSS; + return spcResult.DRAW; + case spcChoice.Scissor: + if (bot.choice === spcChoice.Paper) return spcResult.WIN; + if (bot.choice === spcChoice.Stone) return spcResult.LOSS; + return spcResult.DRAW; + } + } + + private static spcPlayBot(): spcProposition { + return spcProposition.propositions[randomInt(3)]; + } + + private static spcResultProcess( + playerChoice: spcProposition, + botChoice: spcProposition, + result: spcResult + ) { + switch (result) { + case spcResult.WIN: + return { + content: `${botChoice.emoji} ${botChoice.choice} ! Well, noob ${playerChoice.emoji} ${playerChoice.choice} need nerf plz...`, + }; + case spcResult.LOSS: + return { + content: `${botChoice.emoji} ${botChoice.choice} ! Okay bye, Easy!`, + }; + case spcResult.DRAW: + return { + content: `${botChoice.emoji} ${botChoice.choice} ! Ha... Draw...`, + }; + } + } +} diff --git a/examples/button/tsconfig.json b/examples/button/tsconfig.json new file mode 100644 index 00000000..e53b01d4 --- /dev/null +++ b/examples/button/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2017", + "noImplicitAny": false, + "sourceMap": true, + "outDir": "build", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "declaration": true, + "importHelpers": true, + "forceConsistentCasingInFileNames": true, + "lib": ["es2017", "esnext.asynciterable"], + "moduleResolution": "node" + }, + "exclude": ["node_modules", "tests", "examples"] +} diff --git a/examples/command/Main.ts b/examples/command/Main.ts new file mode 100644 index 00000000..9a73675b --- /dev/null +++ b/examples/command/Main.ts @@ -0,0 +1,43 @@ +import "reflect-metadata"; +import { Client } from "../../src"; +import { Intents } from "discord.js"; + +export class Main { + private static _client: Client; + + static get Client(): Client { + return this._client; + } + + static async start() { + this._client = new Client({ + intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES], + classes: [ + `${__dirname}/discords/*.ts`, // glob string to load the classes + `${__dirname}/discords/*.js`, // If you compile your bot, the file extension will be .js + ], + // slashGuilds: [YOUR_GUILD_ID], + requiredByDefault: true, + }); + + // In the login method, you must specify the glob string to load your classes (for the framework). + // In this case that's not necessary because the entry point of your application is this file. + await this._client.login("YOU_TOKEN"); + + this._client.on("messageCreate", (message) => { + this._client.executeCommand(message); + }); + + this._client.once("ready", async () => { + await this._client.initSlashes(); + + console.log("Bot started"); + }); + + this._client.on("interactionCreate", (interaction) => { + this._client.executeInteraction(interaction); + }); + } +} + +Main.start(); diff --git a/examples/command/discords/AppDiscord.ts b/examples/command/discords/AppDiscord.ts new file mode 100644 index 00000000..bfc24782 --- /dev/null +++ b/examples/command/discords/AppDiscord.ts @@ -0,0 +1,41 @@ +import { Discord, Command, CommandOption, CommandMessage } from "../../../src"; + +@Discord() +export abstract class commandTest { + // single whitespace will be used to split options + @Command("math", { argSplitter: " " }) + async cmd( + @CommandOption("num1") num1: number, // + @CommandOption("a") operation: string, // + @CommandOption("num2") num2: number, + message: CommandMessage + ) { + if ( + !num1 || + !operation || + !num2 || + !["+", "-", "*", "/"].includes(operation) + ) + return message.reply( + `**Command Usage:** \`\`${message.command.prefix}${message.command.name} num1 operator num2\`\` ` + // + `\`\`\`${message.command.prefix}${message.command.name} 1 + 3\`\`\`` + ); + + let out = 0; + switch (operation) { + case "+": + out = num1 + num2; + break; + case "-": + out = num1 - num2; + break; + case "*": + out = num1 * num2; + break; + case "/": + out = num1 / num2; + break; + } + message.reply(`${num1} ${operation} ${num2} = ${out}`); + } +} diff --git a/examples/command/tsconfig.json b/examples/command/tsconfig.json new file mode 100644 index 00000000..e53b01d4 --- /dev/null +++ b/examples/command/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2017", + "noImplicitAny": false, + "sourceMap": true, + "outDir": "build", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "declaration": true, + "importHelpers": true, + "forceConsistentCasingInFileNames": true, + "lib": ["es2017", "esnext.asynciterable"], + "moduleResolution": "node" + }, + "exclude": ["node_modules", "tests", "examples"] +} diff --git a/examples/event/Main.ts b/examples/event/Main.ts index 742eb478..ffab46ba 100644 --- a/examples/event/Main.ts +++ b/examples/event/Main.ts @@ -11,19 +11,17 @@ export class Main { static async start() { this._client = new Client({ - intents: [ - Intents.FLAGS.GUILDS, - Intents.FLAGS.GUILD_MESSAGES, - ] + classes: [ + `${__dirname}/discords/*.ts`, // glob string to load the classes + `${__dirname}/discords/*.js`, // If you compile your bot, the file extension will be .js + ], + intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES], + // slashGuilds: [YOUR_GUILD_ID], }); // In the login method, you must specify the glob string to load your classes (for the framework). // In this case that's not necessary because the entry point of your application is this file. - await this._client.login( - "YOUR_TOKEN", - `${__dirname}/discords/*.ts`, // glob string to load the classes - `${__dirname}/discords/*.js` // If you compile your bot, the file extension will be .js - ); + await this._client.login("YOUR_TOKEN"); } } diff --git a/examples/event/discords/AppDiscord.ts b/examples/event/discords/AppDiscord.ts index 63e541bd..131c4485 100644 --- a/examples/event/discords/AppDiscord.ts +++ b/examples/event/discords/AppDiscord.ts @@ -1,12 +1,10 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { Discord, On, Client, ArgsOf } from "../../../src"; @Discord() export abstract class AppDiscord { - @On("message") - onMessage( - [message]: ArgsOf<"message">, - client: Client - ) { + @On("messageCreate") + onMessage([message]: ArgsOf<"messageCreate">, client: Client) { console.log(message.content); } } diff --git a/examples/guards/Main.ts b/examples/guards/Main.ts index 0bc3bc1c..911cfdc3 100644 --- a/examples/guards/Main.ts +++ b/examples/guards/Main.ts @@ -10,30 +10,25 @@ export class Main { static async start() { this._client = new Client({ - intents: [ - Intents.FLAGS.GUILDS, - Intents.FLAGS.GUILD_MESSAGES, + classes: [ + `${__dirname}/discords/*.ts`, // glob string to load the classes + `${__dirname}/discords/*.js`, // If you compile your bot, the file extension will be .js ], - slashGuilds: ["546281071751331840"], + intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES], + // slashGuilds: [YOUR_GUILD_ID], requiredByDefault: true, }); // In the login method, you must specify the glob string to load your classes (for the framework). // In this case that's not necessary because the entry point of your application is this file. - await this._client.login( - "YOUR_TOKEN", - `${__dirname}/discords/*.ts`, // glob string to load the classes - `${__dirname}/discords/*.js` // If you compile your bot, the file extension will be .js - ); + await this._client.login("YOUR_TOKEN"); this._client.once("ready", async () => { - await this._client.clearSlashes(); - await this._client.clearSlashes("546281071751331840"); await this._client.initSlashes(); }); - this._client.on("interaction", (interaction) => { - this._client.executeSlash(interaction); + this._client.on("interactionCreate", (interaction) => { + this._client.executeInteraction(interaction); }); } } diff --git a/examples/guards/discords/AppDiscord.ts b/examples/guards/discords/AppDiscord.ts index d8a0dda4..48e52941 100644 --- a/examples/guards/discords/AppDiscord.ts +++ b/examples/guards/discords/AppDiscord.ts @@ -1,18 +1,20 @@ import { CommandInteraction } from "discord.js"; import { Discord, On, Client, ArgsOf, Guard, Slash } from "../../../src"; -import { Say } from "../guards/Say"; +import { NotBot } from "../guards/NotBot"; @Discord() export abstract class AppDiscord { - @On("message") - @Guard(Say("hello")) - onMessage([message]: ArgsOf<"message">, client: Client) { + @On("messageCreate") + @Guard(NotBot) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onMessage([message]: ArgsOf<"messageCreate">, _client: Client) { console.log(message.content); } @Slash("hello") - @Guard(Say("hello")) - hello(interaction: CommandInteraction, client: Client) { + @Guard(NotBot) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + hello(interaction: CommandInteraction, _client: Client) { console.log(interaction); } } diff --git a/examples/guards/guards/NotBot.ts b/examples/guards/guards/NotBot.ts new file mode 100644 index 00000000..782ea41f --- /dev/null +++ b/examples/guards/guards/NotBot.ts @@ -0,0 +1,22 @@ +import { CommandInteraction, MessageReaction, VoiceState } from "discord.js"; +import { ArgsOf, GuardFunction } from "../../../src"; + +// Example by @AndyClausen + +export const NotBot: GuardFunction< + | ArgsOf<"messageCreate" | "messageReactionAdd" | "voiceStateUpdate"> + | CommandInteraction +> = async (arg, client, next) => { + const argObj = arg instanceof Array ? arg[0] : arg; + const user = + argObj instanceof CommandInteraction + ? argObj.user + : argObj instanceof MessageReaction + ? argObj.message.author + : argObj instanceof VoiceState + ? argObj.member.user + : argObj.author; + if (!user?.bot) { + await next(); + } +}; diff --git a/examples/guards/guards/Say.ts b/examples/guards/guards/Say.ts deleted file mode 100644 index 032898b1..00000000 --- a/examples/guards/guards/Say.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { CommandInteraction } from "discord.js"; -import { ArgsOf, GuardFunction } from "../../../src"; - -export const Say = (text: string) => { - const guard: GuardFunction | CommandInteraction> = async ( - messageOrCommand, - client, - next, - nextObj - ) => { - await next(); - }; - - return guard; -}; diff --git a/examples/menu/Main.ts b/examples/menu/Main.ts new file mode 100644 index 00000000..dbebd39e --- /dev/null +++ b/examples/menu/Main.ts @@ -0,0 +1,39 @@ +import "reflect-metadata"; +import { Client } from "../../src"; +import { Intents } from "discord.js"; + +export class Main { + private static _client: Client; + + static get Client(): Client { + return this._client; + } + + static async start() { + this._client = new Client({ + intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES], + classes: [ + `${__dirname}/discords/*.ts`, // glob string to load the classes + `${__dirname}/discords/*.js`, // If you compile your bot, the file extension will be .js + ], + // slashGuilds: [YOUR_GUILD_ID], + requiredByDefault: true, + }); + + // In the login method, you must specify the glob string to load your classes (for the framework). + // In this case that's not necessary because the entry point of your application is this file. + await this._client.login("YOU_TOKEN"); + + this._client.once("ready", async () => { + await this._client.initSlashes(); + + console.log("Bot started"); + }); + + this._client.on("interactionCreate", (interaction) => { + this._client.executeInteraction(interaction); + }); + } +} + +Main.start(); diff --git a/examples/menu/discords/AppDiscord.ts b/examples/menu/discords/AppDiscord.ts new file mode 100644 index 00000000..97a2e4aa --- /dev/null +++ b/examples/menu/discords/AppDiscord.ts @@ -0,0 +1,54 @@ +import { + CommandInteraction, + MessageActionRow, + SelectMenuInteraction, + MessageSelectMenu, +} from "discord.js"; +import { Discord, Slash, SelectMenu } from "../../../src"; + +const roles = [ + { label: "Principal", value: "principal" }, + { label: "Teacher", value: "teacher" }, + { label: "Student", value: "student" }, +]; + +@Discord() +export abstract class buttons { + @SelectMenu("role-menu") + async handle(interaction: SelectMenuInteraction): Promise { + await interaction.defer(); + + // extract selected value by member + const roleValue = interaction.values?.[0]; + + // if value not found + if (!roleValue) + return await interaction.followUp("invalid role id, select again"); + await interaction.followUp( + `you have selected role: ${ + roles.find((r) => r.value === roleValue).label + }` + ); + return; + } + + @Slash("myroles", { description: "roles menu" }) + async myroles(interaction: CommandInteraction): Promise { + await interaction.defer(); + + // create menu for roels + const menu = new MessageSelectMenu() + .addOptions(roles) + .setCustomId("role-menu"); + + // create a row for meessage actions + const buttonRow = new MessageActionRow().addComponents(menu); + + // send it + interaction.editReply({ + content: "select your role!", + components: [buttonRow], + }); + return; + } +} diff --git a/examples/menu/tsconfig.json b/examples/menu/tsconfig.json new file mode 100644 index 00000000..e53b01d4 --- /dev/null +++ b/examples/menu/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2017", + "noImplicitAny": false, + "sourceMap": true, + "outDir": "build", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "declaration": true, + "importHelpers": true, + "forceConsistentCasingInFileNames": true, + "lib": ["es2017", "esnext.asynciterable"], + "moduleResolution": "node" + }, + "exclude": ["node_modules", "tests", "examples"] +} diff --git a/examples/multiple-discord-instances/Discords/CommandsA.ts b/examples/multiple-discord-instances/Discords/CommandsA.ts index f6597ed9..dc884de8 100644 --- a/examples/multiple-discord-instances/Discords/CommandsA.ts +++ b/examples/multiple-discord-instances/Discords/CommandsA.ts @@ -1,13 +1,10 @@ -import { - Discord, - Slash, -} from "../../../src"; +import { Discord, Slash } from "../../../src"; import { CommandInteraction } from "discord.js"; @Discord() export class CommandsB { @Slash("hello2") - hello2(interaction: CommandInteraction) {; + hello2(interaction: CommandInteraction) { interaction.reply("Hello 2"); } } diff --git a/examples/multiple-discord-instances/Discords/CommandsB.ts b/examples/multiple-discord-instances/Discords/CommandsB.ts index ab9efb76..39cc21b2 100644 --- a/examples/multiple-discord-instances/Discords/CommandsB.ts +++ b/examples/multiple-discord-instances/Discords/CommandsB.ts @@ -1,13 +1,10 @@ -import { - Discord, - Slash, -} from "../../../src"; +import { Discord, Slash } from "../../../src"; import { CommandInteraction } from "discord.js"; @Discord() export class CommandsB { @Slash("hello") - hello(interaction: CommandInteraction) {; + hello(interaction: CommandInteraction) { interaction.reply("Hello 1"); } } diff --git a/examples/multiple-discord-instances/Main.ts b/examples/multiple-discord-instances/Main.ts index 0bc3bc1c..4206ef7f 100644 --- a/examples/multiple-discord-instances/Main.ts +++ b/examples/multiple-discord-instances/Main.ts @@ -10,30 +10,25 @@ export class Main { static async start() { this._client = new Client({ - intents: [ - Intents.FLAGS.GUILDS, - Intents.FLAGS.GUILD_MESSAGES, - ], - slashGuilds: ["546281071751331840"], + intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES], + // slashGuilds: [YOUR_GUILD_ID], requiredByDefault: true, + classes: [ + `${__dirname}/discords/*.ts`, // glob string to load the classes + `${__dirname}/discords/*.js`, // If you compile your bot, the file extension will be .js + ], }); // In the login method, you must specify the glob string to load your classes (for the framework). // In this case that's not necessary because the entry point of your application is this file. - await this._client.login( - "YOUR_TOKEN", - `${__dirname}/discords/*.ts`, // glob string to load the classes - `${__dirname}/discords/*.js` // If you compile your bot, the file extension will be .js - ); + await this._client.login("YOUR_TOKEN"); this._client.once("ready", async () => { - await this._client.clearSlashes(); - await this._client.clearSlashes("546281071751331840"); await this._client.initSlashes(); }); - this._client.on("interaction", (interaction) => { - this._client.executeSlash(interaction); + this._client.on("interactionCreate", (interaction) => { + this._client.executeInteraction(interaction); }); } } diff --git a/examples/slash/Main.ts b/examples/slash/Main.ts index 1857af99..dbebd39e 100644 --- a/examples/slash/Main.ts +++ b/examples/slash/Main.ts @@ -11,32 +11,27 @@ export class Main { static async start() { this._client = new Client({ - intents: [ - Intents.FLAGS.GUILDS, - Intents.FLAGS.GUILD_MESSAGES, + intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES], + classes: [ + `${__dirname}/discords/*.ts`, // glob string to load the classes + `${__dirname}/discords/*.js`, // If you compile your bot, the file extension will be .js ], - // slashGuilds: ["693401527494377482"], + // slashGuilds: [YOUR_GUILD_ID], requiredByDefault: true, }); // In the login method, you must specify the glob string to load your classes (for the framework). // In this case that's not necessary because the entry point of your application is this file. - await this._client.login( - "YOU_TOKEN", - `${__dirname}/discords/*.ts`, // glob string to load the classes - `${__dirname}/discords/*.js` // If you compile your bot, the file extension will be .js - ); + await this._client.login("YOU_TOKEN"); this._client.once("ready", async () => { - await this._client.clearSlashes(); - await this._client.clearSlashes("693401527494377482"); await this._client.initSlashes(); console.log("Bot started"); }); - this._client.on("interaction", (interaction) => { - this._client.executeSlash(interaction); + this._client.on("interactionCreate", (interaction) => { + this._client.executeInteraction(interaction); }); } } diff --git a/examples/slash/discords/AppDiscord.ts b/examples/slash/discords/AppDiscord.ts index 2674db04..b5e9d3ef 100644 --- a/examples/slash/discords/AppDiscord.ts +++ b/examples/slash/discords/AppDiscord.ts @@ -1,28 +1,17 @@ import { CommandInteraction } from "discord.js"; -import { - Discord, - Slash, - Option, - Guild, - Group, - Choices, -} from "../../../src"; +import { Discord, Slash, Option, Guild, Group, Choices } from "../../../src"; enum TextChoices { Hello = "Hello", - "Good Bye" = "GoodBye" + "Good Bye" = "GoodBye", } @Discord() @Guild("693401527494377482") -@Group( - "testing", - "Testing group description", - { - maths: "maths group description", - text: "text group description" - } -) +@Group("testing", "Testing group description", { + maths: "maths group description", + text: "text group description", +}) export abstract class AppDiscord { @Slash("add") @Group("maths") diff --git a/src/Client.ts b/src/Client.ts index 3aeafdd5..9800ce26 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -1,28 +1,29 @@ import { - ApplicationCommandOption, - ApplicationCommandOptionData, + ApplicationCommand, Client as ClientJS, CommandInteraction, CommandInteractionOption, Interaction, + Message, Snowflake, } from "discord.js"; -import * as Glob from "glob"; import { MetadataStorage, - LoadClass, ClientOptions, DiscordEvents, DOn, GuardFunction, } from "."; -import { DDiscord, DOption, DSlash } from "./decorators"; +import { DButton, DDiscord, DOption, DSelectMenu, DSlash } from "./decorators"; +import { DCommand } from "./decorators/classes/DCommand"; import { GuildNotFoundError } from "./errors"; +import { CommandMessage } from "./types/public/CommandMessage"; export class Client extends ClientJS { + private _botId: string; + private _prefix: string | ((message: Message) => Promise); private _silent: boolean; - private _loadClasses: LoadClass[] = []; - private static _requiredByDefault: boolean = false; + private static _requiredByDefault = false; private static _slashGuilds: string[] = []; private static _guards: GuardFunction[] = []; @@ -32,6 +33,21 @@ export class Client extends ClientJS { static set slashGuilds(value) { Client._slashGuilds = value; } + + get prefix() { + return this._prefix; + } + set prefix(value) { + this._prefix = value; + } + + get botId() { + return this._botId; + } + set botId(value) { + this._botId = value; + } + get slashGuilds() { return Client._slashGuilds; } @@ -72,6 +88,27 @@ export class Client extends ClientJS { return Client.slashes; } + static get commands() { + return MetadataStorage.instance.commands as readonly DCommand[]; + } + get commands() { + return Client.commands; + } + + static get buttons() { + return MetadataStorage.instance.buttons as readonly DButton[]; + } + get buttons() { + return Client.buttons; + } + + static get selectMenus() { + return MetadataStorage.instance.selectMenus as readonly DSelectMenu[]; + } + get selectMenus() { + return Client.selectMenus; + } + static get allSlashes() { return MetadataStorage.instance.allSlashes as readonly DSlash[]; } @@ -109,16 +146,21 @@ export class Client extends ClientJS { /** * Create your bot - * @param options { silent: boolean, loadClasses: LoadClass[] } + * @param options { silent: boolean } */ - constructor(options?: ClientOptions) { + constructor(options: ClientOptions) { super(options); + MetadataStorage.classes = [ + ...MetadataStorage.classes, + ...(options?.classes || []), + ]; this._silent = !!options?.silent; - this._loadClasses = options?.classes || []; this.guards = options.guards || []; - this.requiredByDefault = options.requiredByDefault; + this.requiredByDefault = options.requiredByDefault ?? false; this.slashGuilds = options.slashGuilds || []; + this._botId = options.botId || "bot"; + this._prefix = options.prefix || "!"; } /** @@ -126,16 +168,12 @@ export class Client extends ClientJS { * @param token The bot token * @param loadClasses A list of glob path or classes */ - async login(token: string, ...loadClasses: LoadClass[]) { - if (loadClasses.length > 0) { - this._loadClasses = loadClasses; - } - - await this.build(); + async login(token: string) { + await this.decorators.build(); if (!this.silent) { console.log("Events"); - if (this.events.length > 0) { + if (this.events.length) { this.events.map((event) => { const eventName = event.event; console.log(` ${eventName} (${event.classRef.name}.${event.key})`); @@ -147,22 +185,24 @@ export class Client extends ClientJS { console.log(""); console.log("Slashes"); - if (this.slashes.length > 0) { + if (this.slashes.length) { this.slashes.map((slash) => { console.log(` ${slash.name} (${slash.classRef.name}.${slash.key})`); const printOptions = (options: DOption[], depth: number) => { if (!options) return; - + const tab = Array(depth).join(" "); - - options.map((option) => { - console.log(`${tab}${option.name}: ${option.stringType} (${option.classRef.name}.${option.key})`); + + options.forEach((option) => { + console.log( + `${tab}${option.name}: ${option.stringType} (${option.classRef.name}.${option.key})` + ); printOptions(option.options, depth + 1); }); }; - + printOptions(slash.options, 2); - + console.log(""); }); } else { @@ -177,54 +217,170 @@ export class Client extends ClientJS { this.decorators.trigger(on.event, this, true) ); } else { - this.on( - on.event as any, - this.decorators.trigger(on.event, this) - ); + this.on(on.event as any, this.decorators.trigger(on.event, this)); } }); - return await super.login(token); + return super.login(token); } /** * Initialize all the @Slash with their permissions */ - async initSlashes() { - await Promise.all( - this.slashes.map(async (slash) => { - // Init all the @Slash - if (slash.guilds.length > 0) { - // If the @Slash is guild specific, add it to the guild - await Promise.all( - slash.guilds.map(async (guildID) => { - const guild = this.guilds.cache.get(guildID as Snowflake); - - if (!guild) { - throw new GuildNotFoundError(guildID); - } - - const commands = guild.commands; - const command = await commands.create(slash.toObject()); - - if (slash.permissions.length <= 0) return; - - await commands.setPermissions(command, slash.getPermissions()); - }) - ); - } else { - // If the @Slash is global, add it globaly - const commands = this.application.commands; - const command = await commands.create(slash.toObject()); - - // Only available for Guilds - // https://discord.js.org/#/docs/main/master/class/ApplicationCommand?scrollTo=setPermissions - // if (slash.permissions.length <= 0) return; - - // await commands.setPermissions(command, slash.getPermissions()); - } - }) - ); + async initSlashes(options?: { + log: { forGuild: boolean; forGlobal: boolean }; + }) { + // # group guild slashes by guildId + const guildSlashStorage = new Map(); + const guildsSlash = this.slashes.filter((s) => s.guilds?.length); + + // group single guild slashes together + guildsSlash.forEach((s) => { + s.guilds.forEach((guild) => + guildSlashStorage.set(guild, [ + ...(guildSlashStorage.get(guild) ?? []), + s, + ]) + ); + }); + + // run task to add/update/delete slashes for guilds + guildSlashStorage.forEach(async (slashes, key) => { + const guild = await this.guilds.fetch({ guild: key as Snowflake }); + if (!guild) return console.log("guild not found"); + + // fetch already registered command + const existing = await guild.commands.fetch(); + + // filter only unregistered command + const added = slashes.filter( + (s) => + !existing.find((c) => c.name === s.name) && + (!s.botIds.length || s.botIds.includes(this.botId)) + ); + + // filter slashes to update + const updated = slashes + .map<[ApplicationCommand | undefined, DSlash]>((s) => [ + existing.find( + (c) => + c.name === s.name && + (!s.botIds.length || s.botIds.includes(this.botId)) + ), + s, + ]) + .filter<[ApplicationCommand, DSlash]>( + (s): s is [ApplicationCommand, DSlash] => s[0] !== undefined + ); + + // filter slashes to delete + const deleted = existing.filter( + (s) => + !this.slashes.find( + (bs) => + s.name === bs.name && + s.guild && + bs.guilds.includes(s.guild.id) && + (!bs.botIds.length || bs.botIds.includes(this.botId)) + ) + ); + + // log the changes to slashes in console if enabled by options or silent mode is turned off + if (options?.log.forGuild || !this.silent) { + console.log( + `${this.user?.username} >> guild: #${guild} >> command >> adding ${ + added.length + } [${added.map((s) => s.name).join(", ")}]` + ); + + console.log( + `${this.user?.username} >> guild: #${guild} >> command >> deleting ${ + deleted.size + } [${deleted.map((s) => s.name).join(", ")}]` + ); + + console.log( + `${this.user?.username} >> guild: #${guild} >> command >> updating ${updated.length}` + ); + } + + await Promise.all([ + // add and set permissions + ...added.map((s) => + guild.commands.create(s.toObject()).then((cmd) => { + if (s.permissions.length) { + cmd.permissions.set({ permissions: s.permissions }); + } + return cmd; + }) + ), + + // update and set permissions + ...updated.map((s) => + s[0].edit(s[1].toObject()).then((cmd) => { + if (s[1].permissions.length) { + cmd.permissions.set({ permissions: s[1].permissions }); + } + return cmd; + }) + ), + + // delete + ...deleted.map((key) => guild.commands.delete(key)), + ]); + }); + + // # initialize add/update/delete task for global slashes + const existing = (await this.fetchSlash())?.filter((s) => !s.guild); + const slashes = this.slashes.filter((s) => !s.guilds?.length); + if (existing) { + const added = slashes.filter( + (s) => !existing.find((c) => c.name === s.name) + ); + + const updated = slashes + .map<[ApplicationCommand | undefined, DSlash]>((s) => [ + existing.find((c) => c.name === s.name), + s, + ]) + .filter<[ApplicationCommand, DSlash]>( + (s): s is [ApplicationCommand, DSlash] => s[0] !== undefined + ); + + const deleted = existing.filter((c) => + slashes.every((s) => s.name !== c.name) + ); + + // log the changes to slashes in console if enabled by options or silent mode is turned off + if (options?.log.forGlobal || !this.silent) { + console.log( + `${this.user?.username} >> global >> command >> adding ${ + added.length + } [${added.map((s) => s.name).join(", ")}]` + ); + console.log( + `${this.user?.username} >> global >> command >> deleting ${ + deleted.size + } [${deleted.map((s) => s.name).join(", ")}]` + ); + console.log( + `${this.user?.username} >> global >> command >> updating ${updated.length}` + ); + } + + // Only available for Guilds + // https://discord.js.org/#/docs/main/master/class/ApplicationCommand?scrollTo=setPermissions + // if (slash.permissions.length <= 0) return; + + await Promise.all([ + // add + ...added.map((s) => this.application?.commands.create(s.toObject())), + // update + ...updated.map((s) => s[0].edit(s[1].toObject())), + // delete + ...deleted.map((key) => this.application?.commands.delete(key)), + ]); + } } /** @@ -240,7 +396,7 @@ export class Client extends ClientJS { } return await guild.commands.fetch(); } - return await this.application.commands.fetch(); + return await this.application?.commands.fetch(); } /** @@ -248,31 +404,37 @@ export class Client extends ClientJS { * @param guilds The guild IDs (empty -> globaly) */ async clearSlashes(...guilds: string[]) { - if (guilds.length > 0) { + if (guilds.length) { await Promise.all( guilds.map(async (guild) => { // Select and delete the commands of each guild const commands = await this.fetchSlash(guild); - await Promise.all( - commands.map(async (value) => { - await this.guilds.cache.get(guild as Snowflake).commands.delete(value); - }) - ); + if (commands && this.guilds.cache !== undefined) + await Promise.all( + commands.map(async (value) => { + const guildManager = await this.guilds.cache.get( + guild as Snowflake + ); + if (guildManager) guildManager.commands.delete(value); + }) + ); }) ); } else { // Select and delete the commands of each guild const commands = await this.fetchSlash(); - await Promise.all( - commands.map(async (value) => { - await this.application.commands.delete(value); - }) - ); + if (commands) { + await Promise.all( + commands.map(async (value) => { + await this.application?.commands.delete(value); + }) + ); + } } } /** - * Get the group tree of an interaction + * Get the group tree of an interaction * /hello => ["hello"] * /test hello => ["test", "hello"] * /test hello me => ["test", "hello", "me"] @@ -280,19 +442,17 @@ export class Client extends ClientJS { * @returns The group tree */ getInteractionGroupTree(interaction: CommandInteraction) { - const tree = []; + const tree: string[] = []; - const getOptionsTree = ( - option: Partial - ) => { + const getOptionsTree = (option: Partial) => { if (!option) return; if ( - !option.type || + !option.type || option.type === "SUB_COMMAND_GROUP" || option.type === "SUB_COMMAND" ) { - tree.push(option.name); + if (option.name) tree.push(option.name); return getOptionsTree(Array.from(option.options?.values() || [])?.[0]); } }; @@ -300,7 +460,7 @@ export class Client extends ClientJS { getOptionsTree({ name: interaction.commandName, options: interaction.options, - type: undefined + type: undefined, }); return tree; @@ -308,13 +468,13 @@ export class Client extends ClientJS { /** * Return the corresponding @Slash from a tree - * @param tree + * @param tree * @returns The corresponding Slash */ getSlashFromTree(tree: string[]) { // Find the corresponding @Slash return this.allSlashes.find((slash) => { - switch(tree.length) { + switch (tree.length) { case 1: // Simple command /hello return ( @@ -343,11 +503,11 @@ export class Client extends ClientJS { } /** - * Execute the corresponding @Slash command based on an Interaction instance + * Execute the corresponding @Slash @Button @SelectMenu based on an Interaction instance * @param interaction The discord.js interaction instance * @returns void */ - async executeSlash(interaction: Interaction) { + async executeInteraction(interaction: Interaction) { if (!interaction) { if (!this.silent) { console.log("Interaction is undefined"); @@ -355,6 +515,38 @@ export class Client extends ClientJS { return; } + // if interaction is a button + if (interaction.isButton()) { + const button = this.buttons.find((s) => s.id === interaction.customId); + if ( + !button || + (button.guilds.length && + !button.guilds.includes(interaction.guild?.id as string)) || + (button.botIds.length && !button.botIds.includes(this.botId)) + ) + return console.log( + `button interaction not found, interactionID: ${interaction.id} | customID: ${interaction.customId}` + ); + + return button.execute(interaction, this); + } + + // if interaction is a button + if (interaction.isSelectMenu()) { + const menu = this.selectMenus.find((s) => s.id === interaction.customId); + if ( + !menu || + (menu.guilds.length && + !menu.guilds.includes(interaction.guild?.id as string)) || + (menu.botIds.length && !menu.botIds.includes(this.botId)) + ) + return console.log( + `selectMenu interaction not found, interactionID: ${interaction.id} | customID: ${interaction.customId}` + ); + + return menu.execute(interaction, this); + } + // If the interaction isn't a slash command, return if (!interaction.isCommand()) return; @@ -362,18 +554,157 @@ export class Client extends ClientJS { const tree = this.getInteractionGroupTree(interaction); const slash = this.getSlashFromTree(tree); - if (!slash) return; + if (!slash) { + return console.log( + `interaction not found, commandName: ${interaction.commandName}` + ); + } + + if (slash.botIds.length && !slash.botIds.includes(this.botId)) return; // Parse the options values and inject it into the @Slash method - return await slash.execute(interaction, this); + return slash.execute(interaction, this); } /** - * Manually build the app + * Fetch prefix for message + * @param message messsage instance + * @returns prefix */ - async build() { - this.loadClasses(); - await this.decorators.build(); + async getMessagePrefix(message: Message) { + if (typeof this.prefix === "string") return this.prefix; + else return await this.prefix(message); + } + + /** + * + * @param prefix command prefix + * @param message original message + * @returns { isCommand: boolean; commandName?: string; commandArgs?: string } + */ + parseCommand( + prefix: string, + message: Message + ): { isCommand: boolean; commandName: string; commandArgs: string } { + const escapePrefix = prefix.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); + const prefixRegex = RegExp(`^${escapePrefix}`); + const isCommand = prefixRegex.test(message.content); + if (!isCommand) + return { isCommand: false, commandName: "", commandArgs: "" }; + + const contentWithoutPrefix = message.content + .replace(prefixRegex, "") + .trim(); + + const commandName = contentWithoutPrefix.split(" ")[0]; + if (!commandName) + return { isCommand: false, commandName: "", commandArgs: "" }; + + const commandArgs = contentWithoutPrefix.split(" ").splice(1).join(" "); + + return { + isCommand: true, + commandName, + commandArgs, + }; + } + + /** + * Execute the corresponding @Command based on an message instance + * @param message The discord.js message instance + * @returns void + */ + async executeCommand(message: Message) { + if (!message) { + if (!this.silent) { + console.log("message is undefined"); + } + return; + } + + const prefix = await this.getMessagePrefix(message); + if (!prefix) { + if (!this.silent) console.log("command prefix not found"); + return; + } + + const commandInfo = this.parseCommand(prefix, message); + if (!commandInfo.isCommand) return; + + const command = this.commands.find( + (cmd) => cmd.name === commandInfo.commandName + ); + + if (!command) { + if (!this.silent) { + console.log("command not found:", commandInfo.commandName); + } + return; + } + + // validate bot id + if (command.botIds.length && !command.botIds.includes(this.botId)) return; + + // validate guild id + if ( + command.guilds.length && + message.guild?.id && + !command.guilds.includes(message.guild.id) + ) + return; + + // check dm allowed or not + if (!command.directMessage && !message.guild) return; + + // check for member permissions + if (command.defaultPermission) { + // when default perm is on + const permissions = command.permissions.filter( + (perm) => !perm.permission + ); + const userPermissions = permissions.filter( + (perm) => perm.type === "USER" + ); + const rolePermissions = permissions.filter( + (perm) => perm.type === "ROLE" + ); + + const isUserIdAllowed = + userPermissions.some((perm) => perm.id === message.member?.id) || + rolePermissions.some((perm) => + message.member?.roles.cache.has(perm.id) + ); + + // user is not allowed to access this command + if (isUserIdAllowed) return; + } else { + // when default perm is off + const permissions = command.permissions.filter((perm) => perm.permission); + const userPermissions = permissions.filter( + (perm) => perm.type === "USER" + ); + const rolePermissions = permissions.filter( + (perm) => perm.type === "ROLE" + ); + + const isUserIdAllowed = + userPermissions.some((perm) => perm.id === message.member?.id) || + rolePermissions.some((perm) => + message.member?.roles.cache.has(perm.id) + ); + + // user does not have any permission to access this command + if (!isUserIdAllowed) return; + } + + const msg = message as CommandMessage; + msg.command = { + prefix, + object: command, + name: commandInfo.commandName, + argString: commandInfo.commandArgs, + }; + command.execute(msg, this); } /** @@ -382,26 +713,14 @@ export class Client extends ClientJS { * @param params Params to inject * @param once Trigger an once event */ - trigger( - event: DiscordEvents, - params?: any, - once: boolean = false - ): Promise { + trigger(event: DiscordEvents, params?: any, once = false): Promise { return this.decorators.trigger(event, this, once)(params); } - private loadClasses() { - if (!this._loadClasses) { - return; - } - - this._loadClasses.map((file) => { - if (typeof file === "string") { - const files = Glob.sync(file); - files.map((file) => { - require(file); - }); - } - }); + /** + * Manually build the app + */ + async build() { + await this.decorators.build(); } } diff --git a/src/decorators/classes/DButton.ts b/src/decorators/classes/DButton.ts new file mode 100644 index 00000000..fca5617a --- /dev/null +++ b/src/decorators/classes/DButton.ts @@ -0,0 +1,47 @@ +import { Client } from "../.."; +import { Method } from "./Method"; + +export class DButton extends Method { + private _id!: string; + private _guilds!: string[]; + private _botIds!: string[]; + + get botIds() { + return this._botIds; + } + set botIds(value) { + this._botIds = value; + } + + get id() { + return this._id; + } + set id(value) { + this._id = value; + } + + get guilds() { + return this._guilds; + } + set guilds(value) { + this._guilds = value; + } + + protected constructor() { + super(); + } + + static create(id: string, guilds?: string[], botIds?: string[]) { + const button = new DButton(); + + button.id = id; + button.guilds = guilds || Client.slashGuilds; + button.botIds = botIds || []; + + return button; + } + + parseParams() { + return []; + } +} diff --git a/src/decorators/classes/DChoice.ts b/src/decorators/classes/DChoice.ts index d963fca7..9022a4c2 100644 --- a/src/decorators/classes/DChoice.ts +++ b/src/decorators/classes/DChoice.ts @@ -1,9 +1,9 @@ import { ApplicationCommandOptionChoice } from "discord.js"; import { Decorator } from "../classes/Decorator"; -export class DChoice extends Decorator { - private _name: string; - private _value: Type; +export class DChoice extends Decorator { + private _name!: string; + private _value!: string | number; get name() { return this._name; @@ -23,8 +23,8 @@ export class DChoice extends Decorator { super(); } - static create(name: string, value: Type) { - const choice = new DChoice(); + static create(name: string, value: string | number) { + const choice = new DChoice(); choice.name = name; choice.value = value; @@ -35,7 +35,7 @@ export class DChoice extends Decorator { toObject(): ApplicationCommandOptionChoice { return { name: this.name, - value: this.value as any, + value: this.value, }; } } diff --git a/src/decorators/classes/DCommand.ts b/src/decorators/classes/DCommand.ts new file mode 100644 index 00000000..3ec87816 --- /dev/null +++ b/src/decorators/classes/DCommand.ts @@ -0,0 +1,123 @@ +import { ApplicationCommandPermissionData } from "discord.js"; +import { Client } from "../.."; +import { CommandMessage } from "../../types/public/CommandMessage"; +import { DCommandOption } from "./DCommandOption"; +import { Method } from "./Method"; + +export class DCommand extends Method { + private _description!: string; + private _name!: string; + private _defaultPermission!: boolean; + private _directMessage!: boolean; + private _argSplitter!: string; + private _options: DCommandOption[] = []; + private _permissions: ApplicationCommandPermissionData[] = []; + private _guilds!: string[]; + private _botIds!: string[]; + + get botIds() { + return this._botIds; + } + set botIds(value) { + this._botIds = value; + } + + get permissions() { + return this._permissions; + } + set permissions(value) { + this._permissions = value; + } + + get guilds() { + return this._guilds; + } + set guilds(value) { + this._guilds = value; + } + + get argSplitter() { + return this._argSplitter; + } + set argSplitter(value) { + this._argSplitter = value; + } + + get directMessage() { + return this._directMessage; + } + set directMessage(value) { + this._directMessage = value; + } + + get defaultPermission() { + return this._defaultPermission; + } + set defaultPermission(value) { + this._defaultPermission = value; + } + + get name() { + return this._name; + } + set name(value: string) { + this._name = value; + } + + get description() { + return this._description; + } + set description(value: string) { + this._description = value; + } + + get options() { + return this._options; + } + set options(value: DCommandOption[]) { + this._options = value; + } + + protected constructor() { + super(); + } + + static create( + name: string, + description?: string, + argSplitter?: string, + directMessage?: boolean, + defaultPermission?: boolean, + guilds?: string[], + botIds?: string[] + ) { + const cmd = new DCommand(); + + cmd.name = name.toLowerCase(); + cmd.description = description || cmd.name; + cmd.directMessage = directMessage ?? true; + cmd.defaultPermission = defaultPermission ?? true; + cmd.argSplitter = argSplitter ?? " "; + cmd.guilds = guilds || Client.slashGuilds; + cmd.botIds = botIds || []; + + return cmd; + } + + parseParams(message: CommandMessage) { + if (!this.options.length) return []; + const args = message.command.argString.split(this.argSplitter); + + return this.options + .sort((a, b) => (a.index ?? 0) - (b.index ?? 0)) + .map((op, index) => + !args?.[index]?.length + ? undefined + : op.type === "boolean" + ? Boolean(args[index]) + : op.type === "number" + ? Number(args[index]) + : args[index] + ); + } +} diff --git a/src/decorators/classes/DCommandOption.ts b/src/decorators/classes/DCommandOption.ts new file mode 100644 index 00000000..10ab2157 --- /dev/null +++ b/src/decorators/classes/DCommandOption.ts @@ -0,0 +1,46 @@ +import { Decorator } from "./Decorator"; + +export class DCommandOption extends Decorator { + private _name!: string; + private _description!: string; + private _type!: "string" | "number" | "boolean"; + + get name() { + return this._name; + } + set name(value) { + this._name = value; + } + + get type() { + return this._type; + } + set type(value) { + this._type = value; + } + + get description() { + return this._description; + } + set description(value) { + this._description = value; + } + + protected constructor() { + super(); + } + + static create( + name: string, + type: "string" | "number" | "boolean", + description?: string + ) { + const option = new DCommandOption(); + + option._name = name.toLowerCase(); + option._type = type || "string"; + option._description = description || `${name} - ${option.type}`; + + return option; + } +} diff --git a/src/decorators/classes/DDiscord.ts b/src/decorators/classes/DDiscord.ts index 2a45b472..4757f649 100644 --- a/src/decorators/classes/DDiscord.ts +++ b/src/decorators/classes/DDiscord.ts @@ -1,21 +1,23 @@ import { Decorator } from "./Decorator"; -import { - DGuard, - DSlash, - DIService, - DOn -} from "../.."; -import { PermissionType } from "../../types"; +import { DGuard, DSlash, DIService, DOn } from "../.."; +import { DButton } from "./DButton"; +import { DSelectMenu } from "./DSelectMenu"; +import { ApplicationCommandPermissionData } from "discord.js"; +import { DCommand } from "./DCommand"; export class DDiscord extends Decorator { private _guards: DGuard[] = []; + private _buttons: DButton[] = []; + private _selectMenus: DSelectMenu[] = []; private _slashes: DSlash[] = []; + private _commands: DCommand[] = []; private _events: DOn[] = []; - private _description: string; - private _name: string; - private _defaultPermission: boolean = true; - private _permissions: { id: string, type: PermissionType }[] = []; + private _description!: string; + private _name!: string; + private _defaultPermission = true; + private _permissions: ApplicationCommandPermissionData[] = []; private _guilds: string[] = []; + private _botIds: string[] = []; get permissions() { return this._permissions; @@ -24,6 +26,13 @@ export class DDiscord extends Decorator { this._permissions = value; } + get botIds() { + return this._botIds; + } + set botIds(value) { + this._botIds = value; + } + get guilds() { return this._guilds; } @@ -66,6 +75,27 @@ export class DDiscord extends Decorator { this._slashes = value; } + get commands() { + return this._commands; + } + set commands(value) { + this._commands = value; + } + + get buttons() { + return this._buttons; + } + set buttons(value) { + this._buttons = value; + } + + get selectMenus() { + return this._selectMenus; + } + set selectMenus(value) { + this._selectMenus = value; + } + get events() { return this._events; } @@ -83,7 +113,7 @@ export class DDiscord extends Decorator { static create(name: string) { const discord = new DDiscord(); - + discord.name = name; return discord; diff --git a/src/decorators/classes/DGroup.ts b/src/decorators/classes/DGroup.ts index 9e67849f..67d79aa1 100644 --- a/src/decorators/classes/DGroup.ts +++ b/src/decorators/classes/DGroup.ts @@ -1,9 +1,8 @@ -import { InstanceOf, ToInterface } from "../../types"; import { Decorator } from "./Decorator"; export class DGroup extends Decorator { - name: string; - infos: Partial; + name!: string; + infos!: Partial; protected constructor() { super(); @@ -13,7 +12,7 @@ export class DGroup extends Decorator { const group = new DGroup(); group.name = name.toLowerCase(); - group.infos = infos ||Β {} as any; + group.infos = infos || ({} as any); return group; } diff --git a/src/decorators/classes/DGuard.ts b/src/decorators/classes/DGuard.ts index a8aee3de..236cfbe5 100644 --- a/src/decorators/classes/DGuard.ts +++ b/src/decorators/classes/DGuard.ts @@ -2,7 +2,7 @@ import { Decorator } from "./Decorator"; import { GuardFunction } from "../.."; export class DGuard extends Decorator { - protected _fn: GuardFunction; + protected _fn!: GuardFunction; get fn() { return this._fn; diff --git a/src/decorators/classes/DOn.ts b/src/decorators/classes/DOn.ts index 7797f985..c16bca45 100644 --- a/src/decorators/classes/DOn.ts +++ b/src/decorators/classes/DOn.ts @@ -1,11 +1,17 @@ -import { - DiscordEvents -} from "../.."; +import { DiscordEvents } from "../.."; import { Method } from "./Method"; export class DOn extends Method { - protected _event: DiscordEvents; - protected _once: boolean; + protected _event!: DiscordEvents; + protected _once!: boolean; + protected _botIds!: string[]; + + get botIds() { + return this._botIds; + } + set botIds(value: string[]) { + this._botIds = value; + } get event() { return this._event; @@ -25,11 +31,12 @@ export class DOn extends Method { super(); } - static create(event: DiscordEvents, once: boolean) { + static create(event: DiscordEvents, once: boolean, botIds?: string[]) { const on = new DOn(); on._event = event; on._once = once; + on._botIds = botIds ?? []; return on; } diff --git a/src/decorators/classes/DOption.ts b/src/decorators/classes/DOption.ts index 59aa5686..07075947 100644 --- a/src/decorators/classes/DOption.ts +++ b/src/decorators/classes/DOption.ts @@ -9,16 +9,22 @@ import { VoiceChannel, } from "discord.js"; import { Decorator } from "../classes/Decorator"; -import { DChoice, Client, OptionValueType, StringOptionType, OptionType } from "../.."; +import { + DChoice, + Client, + OptionValueType, + StringOptionType, + OptionType, +} from "../.."; export class DOption extends Decorator { - private _required: boolean = false; - private _name: string; - private _type: OptionValueType; - private _description: string; + private _required = false; + private _name!: string; + private _type!: OptionValueType; + private _description!: string; private _choices: DChoice[] = []; private _options: DOption[] = []; - private _isNode: boolean = false; + private _isNode = false; get isNode() { return this._isNode; @@ -93,6 +99,8 @@ export class DOption extends Decorator { return OptionType.USER; case ClientUser: return OptionType.USER; + default: + return OptionType.STRING; } } @@ -126,7 +134,9 @@ export class DOption extends Decorator { type: this.stringType, required: this.required, choices: this.choices.map((choice) => choice.toObject()), - options: [...this.options].reverse().map((option) => option.toObject()) as ApplicationCommandOption[] + options: [...this.options] + .reverse() + .map((option) => option.toObject()) as ApplicationCommandOption[], }; if (!this.isNode) { diff --git a/src/decorators/classes/DSelectMenu.ts b/src/decorators/classes/DSelectMenu.ts new file mode 100644 index 00000000..65f738fa --- /dev/null +++ b/src/decorators/classes/DSelectMenu.ts @@ -0,0 +1,47 @@ +import { Client } from "../.."; +import { Method } from "./Method"; + +export class DSelectMenu extends Method { + private _id!: string; + private _guilds!: string[]; + private _botIds!: string[]; + + get botIds() { + return this._botIds; + } + set botIds(value) { + this._botIds = value; + } + + get id() { + return this._id; + } + set id(value) { + this._id = value; + } + + get guilds() { + return this._guilds; + } + set guilds(value) { + this._guilds = value; + } + + protected constructor() { + super(); + } + + static create(id: string, guilds?: string[], botIds?: string[]) { + const menu = new DSelectMenu(); + + menu.id = id; + menu.guilds = guilds || Client.slashGuilds; + menu.botIds = botIds || []; + + return menu; + } + + parseParams() { + return []; + } +} diff --git a/src/decorators/classes/DSlash.ts b/src/decorators/classes/DSlash.ts index f9663185..6284c917 100644 --- a/src/decorators/classes/DSlash.ts +++ b/src/decorators/classes/DSlash.ts @@ -3,20 +3,27 @@ import { ApplicationCommandPermissionData, CommandInteraction, CommandInteractionOption, - Snowflake, } from "discord.js"; -import { DOption, Client, SubValueType, PermissionType } from "../.."; +import { DOption, Client } from "../.."; import { Method } from "./Method"; export class DSlash extends Method { - private _description: string; - private _name: string; - private _defaultPermission: boolean = true; + private _description!: string; + private _name!: string; + private _defaultPermission!: boolean; private _options: DOption[] = []; - private _permissions: { id: string, type: PermissionType }[] = []; - private _guilds: string[]; - private _group: string; - private _subgroup: string; + private _permissions: ApplicationCommandPermissionData[] = []; + private _guilds!: string[]; + private _group!: string; + private _subgroup!: string; + private _botIds!: string[]; + + get botIds() { + return this._botIds; + } + set botIds(value) { + this._botIds = value; + } get group() { return this._group; @@ -81,15 +88,17 @@ export class DSlash extends Method { static create( name: string, description?: string, - defaultPermission: boolean = true, - guilds?: string[] + defaultPermission?: boolean, + guilds?: string[], + botIds?: string[] ) { const slash = new DSlash(); slash.name = name.toLowerCase(); slash.description = description || slash.name; - slash.defaultPermission = defaultPermission; + slash.defaultPermission = defaultPermission ?? true; slash.guilds = guilds || Client.slashGuilds; + slash.botIds = botIds || []; return slash; } @@ -99,21 +108,17 @@ export class DSlash extends Method { this.name, "SUB_COMMAND", this.description - ).decorate( - this.classRef, - this.key, - this.method, - this.from, - this.index - ); + ).decorate(this.classRef, this.key, this.method, this.from, this.index); option.options = this.options; return option; } toObject(): ApplicationCommandData { - const options = [...this.options].reverse().map((option) => option.toObject()); - + const options = [...this.options] + .reverse() + .map((option) => option.toObject()); + return { name: this.name, description: this.description, @@ -122,17 +127,11 @@ export class DSlash extends Method { }; } - getPermissions(): ApplicationCommandPermissionData[] { - return this.permissions.map((permission) => ({ - permission: true, - id: permission.id as Snowflake, - type: permission.type, - })); - } - - getLastNestedOption(options: Map): CommandInteractionOption[] { + getLastNestedOption( + options: Map + ): CommandInteractionOption[] { const arrOptions = Array.from(options?.values()); - + if (!arrOptions?.[0]?.options) { return arrOptions; } @@ -143,10 +142,8 @@ export class DSlash extends Method { parseParams(interaction: CommandInteraction) { const options = this.getLastNestedOption(interaction.options); - const values = this.options.map((opt, index) => { - return options[index]?.value; - }); - - return values; + return this.options + .sort((a, b) => (a.index ?? 0) - (b.index ?? 0)) + .map((op) => options.find((o) => o.name === op.name)?.value); } } diff --git a/src/decorators/classes/Decorator.ts b/src/decorators/classes/Decorator.ts index 7103595b..06a27166 100644 --- a/src/decorators/classes/Decorator.ts +++ b/src/decorators/classes/Decorator.ts @@ -1,11 +1,11 @@ import { DecoratorUtils } from "../../logic"; export class Decorator { - protected _classRef: Function; - protected _from: Function; - protected _key: string; - protected _method: Function; - protected _index: number = undefined; + protected _classRef!: Function; + protected _from!: Function; + protected _key!: string; + protected _method!: Function; + protected _index: number | undefined = undefined; get index() { return this._index; @@ -39,6 +39,7 @@ export class Decorator { } protected constructor() { + // empty constructor } decorateUnknown( @@ -58,7 +59,7 @@ export class Decorator { return this.decorate( finalClassRef, - finalKey, + finalKey as string, finalMethod, finalClassRef, index @@ -75,13 +76,15 @@ export class Decorator { this._from = from || classRef; this._classRef = classRef; this._key = key; - this._method = method; - this._index = index; + this._method = method as Function; + this._index = index as number; this.update(); return this; } - update() {} + update() { + // empty function + } } diff --git a/src/decorators/classes/Method.ts b/src/decorators/classes/Method.ts index 3381703e..d96b4fe1 100644 --- a/src/decorators/classes/Method.ts +++ b/src/decorators/classes/Method.ts @@ -1,8 +1,8 @@ -import { DGuard, Client, DDiscord, DIService } from "../.."; +import { DGuard, Client, DDiscord } from "../.."; import { Decorator } from "./Decorator"; export abstract class Method extends Decorator { - protected _discord: DDiscord; + protected _discord!: DDiscord; protected _guards: DGuard[] = []; get discord() { @@ -37,13 +37,15 @@ export abstract class Method extends Decorator { * The guards that decorate the method (this) */ get guards() { - const clientGuards = Client.guards.map((guard) => DGuard.create(guard.bind(undefined))); + const clientGuards = Client.guards.map((guard) => + DGuard.create(guard.bind(undefined)) + ); return [ ...clientGuards, ...this.discord.guards, ...this._guards, - DGuard.create(this._method.bind(this._discord.instance)) + DGuard.create(this._method.bind(this._discord.instance)), ]; } set guards(value) { @@ -60,11 +62,7 @@ export abstract class Method extends Decorator { * Execute a guard with params */ getGuardFunction(): (...params: any[]) => Promise { - const next = async ( - params: any, - index: number, - paramsToNext: any - ) => { + const next = async (params: any, index: number, paramsToNext: any) => { const nextFn = () => next(params, index + 1, paramsToNext); const guardToExecute = this.guards[index]; let res: any; @@ -73,18 +71,14 @@ export abstract class Method extends Decorator { // If it's the main method res = await (guardToExecute.fn as any)( // method(...ParsedOptions, [Interaction, Client], ...) => method(...ParsedOptions, Interaction, Client, ...) - ...this.parseParams(...params as any), + ...this.parseParams(...(params as any)), ...params, paramsToNext ); } else { // If it's the guards // method([Interaction, Client]) - res = await (guardToExecute.fn as any)( - ...params, - nextFn, - paramsToNext - ); + res = await (guardToExecute.fn as any)(...params, nextFn, paramsToNext); } if (res) { @@ -96,4 +90,4 @@ export abstract class Method extends Decorator { return (...params: any[]) => next(params, 0, {}); } -} \ No newline at end of file +} diff --git a/src/decorators/classes/Modifier.ts b/src/decorators/classes/Modifier.ts index 7ee988ae..6f6f138b 100644 --- a/src/decorators/classes/Modifier.ts +++ b/src/decorators/classes/Modifier.ts @@ -6,8 +6,8 @@ export type ModifyFunction = ( ) => any; export class Modifier extends Decorator { - private _toModify: ModifyFunction; - private _modifyTypes: any[]; + private _toModify!: ModifyFunction; + private _modifyTypes!: any[]; protected constructor() { super(); @@ -31,7 +31,7 @@ export class Modifier extends Decorator { * that are on the targets type of modification * @param modifiers The modifier list * @param originals The list of objects to modify - * @returns + * @returns */ static async applyFromModifierListToList( modifiers: Modifier[], @@ -43,7 +43,7 @@ export class Modifier extends Decorator { let linked = DecoratorUtils.getLinkedObjects(modifier, originals); // Filter the linked objects to match the target types of modification - linked = linked.filter((l) => + linked = linked.filter((l) => modifier._modifyTypes.includes((l as Object).constructor) ); diff --git a/src/decorators/decorators/Bot.ts b/src/decorators/decorators/Bot.ts new file mode 100644 index 00000000..9ad09c82 --- /dev/null +++ b/src/decorators/decorators/Bot.ts @@ -0,0 +1,51 @@ +import { MetadataStorage, Modifier } from "../.."; +import { DButton } from "../classes/DButton"; +import { DCommand } from "../classes/DCommand"; +import { DDiscord } from "../classes/DDiscord"; +import { DOn } from "../classes/DOn"; +import { DSelectMenu } from "../classes/DSelectMenu"; +import { DSlash } from "../classes/DSlash"; + +export function Bot(botID: string); +export function Bot(...botIDs: string[]); +export function Bot(...botIDs: string[]) { + return ( + target: Function, + key: string, + descriptor: PropertyDescriptor + ): void => { + MetadataStorage.instance.addModifier( + Modifier.create< + DSlash | DCommand | DDiscord | DButton | DSelectMenu | DOn + >( + (original) => { + original.botIds = [ + ...original.botIds, + ...botIDs.filter((botID) => !original.botIds.includes(botID)), + ]; + + if (original instanceof DDiscord) { + [ + ...original.slashes, + ...original.commands, + ...original.buttons, + ...original.selectMenus, + ...original.events, + ].forEach((ob) => { + ob.botIds = [ + ...ob.botIds, + ...botIDs.filter((botID) => !ob.botIds.includes(botID)), + ]; + }); + } + }, + DSlash, + DCommand, + DDiscord, + DButton, + DSelectMenu, + DOn + ).decorateUnknown(target, key, descriptor) + ); + }; +} diff --git a/src/decorators/decorators/Button.ts b/src/decorators/decorators/Button.ts new file mode 100644 index 00000000..681497f3 --- /dev/null +++ b/src/decorators/decorators/Button.ts @@ -0,0 +1,16 @@ +import { MetadataStorage, DButton } from "../.."; + +export function Button(id: string); +export function Button( + id: string, + params?: { guilds?: string[]; botIds?: [] } +) { + return (target: Object, key: string) => { + const button = DButton.create(id, params?.guilds, params?.botIds).decorate( + target.constructor, + key, + target[key] + ); + MetadataStorage.instance.addButton(button); + }; +} diff --git a/src/decorators/decorators/Choice.ts b/src/decorators/decorators/Choice.ts index 051f638d..40db1cf4 100644 --- a/src/decorators/decorators/Choice.ts +++ b/src/decorators/decorators/Choice.ts @@ -3,13 +3,10 @@ import { MetadataStorage, DChoice, DOption, Modifier } from "../.."; export function Choice(name: string, value: number); export function Choice(name: string, value: string); export function Choice(name: string, value: string | number) { - return async (target: Object, key: string, index: number) => { + return (target: Object, key: string, index: number) => { MetadataStorage.instance.addModifier( - Modifier.create(async (original) => { - original.choices = [ - ...original.choices, - DChoice.create(name, value), - ]; + Modifier.create((original) => { + original.choices = [...original.choices, DChoice.create(name, value)]; }, DOption).decorate( target.constructor, key, diff --git a/src/decorators/decorators/Choices.ts b/src/decorators/decorators/Choices.ts index f3043bb3..941245f2 100644 --- a/src/decorators/decorators/Choices.ts +++ b/src/decorators/decorators/Choices.ts @@ -1,17 +1,20 @@ -import { MetadataStorage, DChoice, DOption, Modifier, ChoicesType } from "../.."; +import { + MetadataStorage, + DChoice, + DOption, + Modifier, + ChoicesType, +} from "../.."; export function Choices(choices: ChoicesType); export function Choices(choices: ChoicesType) { - return async (target: Object, key: string, index: number) => { + return (target: Object, key: string, index: number) => { MetadataStorage.instance.addModifier( - Modifier.create(async (original) => { + Modifier.create((original) => { const arrayChoices = Object.keys(choices).map((key) => { return DChoice.create(key, choices[key]); }); - original.choices = [ - ...original.choices, - ...arrayChoices, - ]; + original.choices = [...original.choices, ...arrayChoices]; }, DOption).decorate( target.constructor, key, diff --git a/src/decorators/decorators/Command.ts b/src/decorators/decorators/Command.ts new file mode 100644 index 00000000..f8dbaf87 --- /dev/null +++ b/src/decorators/decorators/Command.ts @@ -0,0 +1,28 @@ +import { MetadataStorage } from "../.."; +import { DCommand } from "../classes/DCommand"; +import { CommandParams } from "../params/CommandParams"; + +const testName = RegExp(/^[a-z0-9]+$/); + +export function Command(); +export function Command(name: string); +export function Command(name: string, params: CommandParams); +export function Command(name?: string, params?: CommandParams) { + return (target: Object, key: string) => { + name = name || key; + name = name.toLocaleLowerCase(); + if (!testName.test(name)) throw Error("invalid command name"); + + const cmd = DCommand.create( + name, + params?.description, + params?.argSplitter, + params?.directMessage, + params?.defaultPermission, + params?.guilds, + params?.botIds + ).decorate(target.constructor, key, target[key]); + + MetadataStorage.instance.addCommand(cmd); + }; +} diff --git a/src/decorators/decorators/CommandOption.ts b/src/decorators/decorators/CommandOption.ts new file mode 100644 index 00000000..3830169b --- /dev/null +++ b/src/decorators/decorators/CommandOption.ts @@ -0,0 +1,43 @@ +import "reflect-metadata"; +import { MetadataStorage, Modifier } from "../.."; +import { DCommand } from "../classes/DCommand"; +import { DCommandOption } from "../classes/DCommandOption"; + +export function CommandOption(name: string); +export function CommandOption( + name: string, + params: { description?: string; type?: "string" | "number" | "boolean" } +); +export function CommandOption( + name: string, + params?: { description?: string; type?: "string" | "number" | "boolean" } +) { + return (target: Object, key: string, index: number) => { + const type = + params?.type ?? + (( + Reflect.getMetadata("design:paramtypes", target, key)[index] + .name as string + ).toLowerCase() as "string" | "number" | "boolean"); + + const option = DCommandOption.create( + name || key, + type, + params?.description + ).decorate(target.constructor, key, target[key], target.constructor, index); + + MetadataStorage.instance.addModifier( + Modifier.create((original) => { + original.options = [...original.options, option]; + }, DCommand).decorate( + target.constructor, + key, + target[key], + target.constructor, + index + ) + ); + + MetadataStorage.instance.addCommandOption(option); + }; +} diff --git a/src/decorators/decorators/DefaultPermission.ts b/src/decorators/decorators/DefaultPermission.ts new file mode 100644 index 00000000..886ecf71 --- /dev/null +++ b/src/decorators/decorators/DefaultPermission.ts @@ -0,0 +1,27 @@ +import { MetadataStorage, Modifier } from "../.."; +import { DCommand } from "../classes/DCommand"; +import { DDiscord } from "../classes/DDiscord"; +import { DSlash } from "../classes/DSlash"; + +export function DefaultPermission(); +export function DefaultPermission(permission: boolean); +export function DefaultPermission(permission?: boolean) { + return (target: Object, key: string, descriptor: PropertyDescriptor) => { + MetadataStorage.instance.addModifier( + Modifier.create( + (original) => { + original.defaultPermission = permission ?? true; + + if (original instanceof DDiscord) { + [...original.slashes, ...original.commands].forEach((obj) => { + obj.defaultPermission = permission ?? true; + }); + } + }, + DSlash, + DCommand, + DDiscord + ).decorateUnknown(target, key, descriptor) + ); + }; +} diff --git a/src/decorators/decorators/Description.ts b/src/decorators/decorators/Description.ts index 37510db4..fac2a270 100644 --- a/src/decorators/decorators/Description.ts +++ b/src/decorators/decorators/Description.ts @@ -1,4 +1,5 @@ -import { MetadataStorage, Modifier, DDiscord } from "../.."; +import { MetadataStorage, Modifier } from "../.."; +import { DCommand } from "../classes/DCommand"; import { DSlash } from "../classes/DSlash"; export function Description(description: string); @@ -9,9 +10,13 @@ export function Description(description: string) { descriptor: PropertyDescriptor ): void => { MetadataStorage.instance.addModifier( - Modifier.create(async (original) => { - original.description = description; - }, DSlash).decorate(target.constructor, key, descriptor.value) + Modifier.create( + (original) => { + original.description = description; + }, + DSlash, + DCommand + ).decorate(target.constructor, key, descriptor.value) ); }; } diff --git a/src/decorators/decorators/Discord.ts b/src/decorators/decorators/Discord.ts index 985ba650..25cdb652 100644 --- a/src/decorators/decorators/Discord.ts +++ b/src/decorators/decorators/Discord.ts @@ -1,75 +1,13 @@ -import * as Glob from "glob"; -import { - MetadataStorage, - DiscordParams, - DDiscord, - DIService, -} from "../.."; +import { MetadataStorage, DDiscord } from "../.."; /** - * Import the commands when using @Discord({ imports: [...] }) - * command.hidden / on.hidden is set to true of the command was imported this way * @param classType The class of the imported command / on * @param target The class of the destination (the class that is decorated by @Discord) */ -function importCommand(classType: Function, target: Function) { - DIService.instance.addService(classType); - - const ons = MetadataStorage.instance.events.filter((on) => { - return on.classRef === classType; - }); - - ons.map((event) => { - // Set the property hidden to true when - // it's imported - event.from = target; - }); -} - -export function Discord(); -// export function Discord(params: DiscordParams); -export function Discord(params?: DiscordParams) { - return (target: Function, key: string) => { - if (params?.import) { - let importCommands = params?.import || []; - if (!Array.isArray(importCommands)) { - importCommands = [importCommands]; - } - - // For the commands that were imported like @Discord({ import: [...] }) - importCommands.map((cmd) => { - if (typeof cmd === "string") { - // For the commands that were imported like @Discord({ import: ["*.ts"] }) - const files = Glob.sync(cmd); - files.map((file) => { - let classType; - const classImport = require(file); - if (classImport.default) { - // If the class was exported by default it sets - // the classType to the default export - // export default MyClass - classType = classImport.default; - } else { - // If the class was exported inside a file it sets - // the classType to the first export - // export MyClass - const key = Object.keys(classImport)[0]; - classType = classImport[key]; - } - importCommand(classType, target); - }); - } else { - // For the commands that were imported like @Discord({ import: [MyClass] }) - importCommand(cmd, target); - } - }); - } - - const instance = DDiscord.create(target.name).decorate( - target, - target.name - ); +export function Discord() { + return (target: Function) => { + const instance = DDiscord.create(target.name).decorate(target, target.name); MetadataStorage.instance.addDiscord(instance); }; } diff --git a/src/decorators/decorators/Group.ts b/src/decorators/decorators/Group.ts index be608eb2..0c365d50 100644 --- a/src/decorators/decorators/Group.ts +++ b/src/decorators/decorators/Group.ts @@ -7,7 +7,11 @@ export function Group(group: string); export function Group(subCommands: SubCommand); export function Group(group: string, description: string); export function Group(group: string, subCommands: SubCommand); -export function Group(group: string, description: string, subCommands: SubCommand); +export function Group( + group: string, + description: string, + subCommands: SubCommand +); export function Group( groupOrSubcommands: string | SubCommand, subCommandsOrDescription?: SubCommand | string, @@ -20,41 +24,38 @@ export function Group( ) => { // Detect the type of parameters for overloading const isGroup = typeof groupOrSubcommands === "string"; - const group = isGroup ? (groupOrSubcommands as string).toLocaleLowerCase() : undefined; + const group = isGroup + ? (groupOrSubcommands as string).toLocaleLowerCase() + : undefined; const isDescription = typeof subCommandsOrDescription === "string"; - const description = isDescription ? subCommandsOrDescription as string : undefined; + const description = isDescription + ? (subCommandsOrDescription as string) + : undefined; if (subCommandsOrDescription !== undefined && !isDescription) { subCommands = subCommandsOrDescription as SubCommand; } - subCommands = isGroup ? subCommands : groupOrSubcommands as SubCommand; + subCommands = isGroup ? subCommands : (groupOrSubcommands as SubCommand); if (!descriptor) { // Add the group to groups if @Group decorate a class if (group) { const group = DGroup.create( - groupOrSubcommands as string || key, + (groupOrSubcommands as string) ?? key, { description } - ).decorate( - target as Function, - (target as Function).name - ); + ).decorate(target as Function, (target as Function).name); MetadataStorage.instance.addGroup(group); } // Create a subgroup if @Group decorate a method if (subCommands) { - Object.keys(subCommands).map((key) => { - const group = DGroup.create( - key, - { description: subCommands[key] } - ).decorate( - target as Function, - (target as Function).name - ); + Object.keys(subCommands).forEach((key) => { + const group = DGroup.create(key, { + description: subCommands?.[key], + }).decorate(target as Function, (target as Function).name); MetadataStorage.instance.addSubGroup(group); }); @@ -62,9 +63,9 @@ export function Group( } else { // If @Group decorate a method edit the method and add it to subgroup MetadataStorage.instance.addModifier( - Modifier.create(async (original) => { + Modifier.create((original) => { original.subgroup = group as string; - }, DSlash).decorate(target.constructor, key) + }, DSlash).decorate(target.constructor, key as string) ); } }; diff --git a/src/decorators/decorators/Guard.ts b/src/decorators/decorators/Guard.ts index 0b8de8d1..89e61bce 100644 --- a/src/decorators/decorators/Guard.ts +++ b/src/decorators/decorators/Guard.ts @@ -1,6 +1,15 @@ -import { MetadataStorage, GuardFunction, DGuard, Modifier, Method } from "../.."; +import { + MetadataStorage, + GuardFunction, + DGuard, + Modifier, + Method, +} from "../.."; +import { DButton } from "../classes/DButton"; +import { DCommand } from "../classes/DCommand"; import { DDiscord } from "../classes/DDiscord"; import { DOn } from "../classes/DOn"; +import { DSelectMenu } from "../classes/DSelectMenu"; import { DSlash } from "../classes/DSlash"; export function Guard( @@ -12,17 +21,21 @@ export function Guard( descriptor?: PropertyDescriptor ): void => { const guards = fns.map((fn) => { - return DGuard.create(fn).decorateUnknown( - target, - key, - descriptor - ); + return DGuard.create(fn).decorateUnknown(target, key, descriptor); }); MetadataStorage.instance.addModifier( - Modifier.create(async (original) => { - original.guards = guards; - }, DSlash, DOn, DDiscord).decorateUnknown(target, key, descriptor) + Modifier.create( + (original) => { + original.guards = guards; + }, + DSelectMenu, + DButton, + DSlash, + DCommand, + DOn, + DDiscord + ).decorateUnknown(target, key, descriptor) ); }; } diff --git a/src/decorators/decorators/Guild.ts b/src/decorators/decorators/Guild.ts index 13d8cd87..41940417 100644 --- a/src/decorators/decorators/Guild.ts +++ b/src/decorators/decorators/Guild.ts @@ -1,5 +1,8 @@ import { MetadataStorage, Modifier } from "../.."; +import { DButton } from "../classes/DButton"; +import { DCommand } from "../classes/DCommand"; import { DDiscord } from "../classes/DDiscord"; +import { DSelectMenu } from "../classes/DSelectMenu"; import { DSlash } from "../classes/DSlash"; export function Guild(guildID: string); @@ -11,21 +14,35 @@ export function Guild(...guildIDs: string[]) { descriptor: PropertyDescriptor ): void => { MetadataStorage.instance.addModifier( - Modifier.create(async (original) => { - original.guilds = [ - ...original.guilds, - ...guildIDs - ]; + Modifier.create( + (original) => { + original.guilds = [ + ...original.guilds, + ...guildIDs.filter((guildID) => !original.guilds.includes(guildID)), + ]; - if (original instanceof DDiscord) { - original.slashes.map((slash) => { - slash.guilds = [ - ...slash.guilds, - ...guildIDs - ]; - }); - } - }, DSlash, DDiscord).decorateUnknown(target, key, descriptor) + if (original instanceof DDiscord) { + [ + ...original.slashes, + ...original.commands, + ...original.buttons, + ...original.selectMenus, + ].forEach((slash) => { + slash.guilds = [ + ...slash.guilds, + ...guildIDs.filter( + (guildID) => !slash.guilds.includes(guildID) + ), + ]; + }); + } + }, + DSlash, + DCommand, + DDiscord, + DButton, + DSelectMenu + ).decorateUnknown(target, key, descriptor) ); }; } diff --git a/src/decorators/decorators/Name.ts b/src/decorators/decorators/Name.ts index 530f7336..46782347 100644 --- a/src/decorators/decorators/Name.ts +++ b/src/decorators/decorators/Name.ts @@ -1,4 +1,5 @@ import { MetadataStorage, DSlash, Modifier, DDiscord } from "../.."; +import { DCommand } from "../classes/DCommand"; export function Name(name: string); export function Name(name: string) { @@ -8,9 +9,14 @@ export function Name(name: string) { descriptor?: PropertyDescriptor ): void => { MetadataStorage.instance.addModifier( - Modifier.create(async (original) => { - original.name = name; - }, DSlash, DDiscord).decorateUnknown(target, key, descriptor) + Modifier.create( + (original) => { + original.name = name; + }, + DSlash, + DCommand, + DDiscord + ).decorateUnknown(target, key, descriptor) ); }; } diff --git a/src/decorators/decorators/On.ts b/src/decorators/decorators/On.ts index 57bfc09b..5ccb0a4f 100644 --- a/src/decorators/decorators/On.ts +++ b/src/decorators/decorators/On.ts @@ -1,20 +1,23 @@ import { MetadataStorage, DiscordEvents, DOn } from "../.."; +import { EventParams } from "../params/EventParams"; /** * Trigger a discord event * @link https://github.com/OwenCalvin/discord.ts#client-payload-injection * @param event The discord event to trigger */ -export function On(event: DiscordEvents) { +export function On(event: DiscordEvents); +export function On(event: DiscordEvents, params?: EventParams); +export function On(event: DiscordEvents, params?: EventParams) { return ( target: Object, key: string, descriptor?: PropertyDescriptor ): void => { - const on = DOn.create(event, false).decorate( + const on = DOn.create(event, false, params?.botIds).decorate( target.constructor, key, - descriptor.value + descriptor?.value ); MetadataStorage.instance.addOn(on); diff --git a/src/decorators/decorators/Once.ts b/src/decorators/decorators/Once.ts index 748150d7..5701e014 100644 --- a/src/decorators/decorators/Once.ts +++ b/src/decorators/decorators/Once.ts @@ -1,17 +1,20 @@ import { MetadataStorage, DiscordEvents, DOn } from "../.."; +import { EventParams } from "../params/EventParams"; /** * Trigger a discord event only once * @link https://github.com/OwenCalvin/discord.ts#client-payload-injection * @param event The discord event to trigger */ -export function Once(event: DiscordEvents) { +export function Once(event: DiscordEvents); +export function Once(event: DiscordEvents, params?: EventParams); +export function Once(event: DiscordEvents, params?: EventParams) { return ( target: Object, key: string, descriptor: PropertyDescriptor ): void => { - const on = DOn.create(event, true).decorate( + const on = DOn.create(event, true, params?.botIds).decorate( target.constructor, key, descriptor.value diff --git a/src/decorators/decorators/Option.ts b/src/decorators/decorators/Option.ts index c9295dce..3b7476a4 100644 --- a/src/decorators/decorators/Option.ts +++ b/src/decorators/decorators/Option.ts @@ -2,7 +2,6 @@ import "reflect-metadata"; import { MetadataStorage, DOption, - OptionType, OptionValueType, OptionParams, Modifier, @@ -10,53 +9,28 @@ import { import { DSlash } from "../classes/DSlash"; export function Option(name: string); -export function Option(name: string, type: OptionValueType | OptionType); export function Option(name: string, params: OptionParams); -export function Option( - name: string, - type: OptionValueType | OptionType, - params: OptionParams -); -export function Option( - name: string, - typeOrParams?: OptionParams | OptionValueType | OptionType, - params?: OptionParams -) { +export function Option(name: string, params?: OptionParams) { return (target: Object, key: string, index: number) => { - const isParams = typeof typeOrParams === "object"; - let finalParams: OptionParams = params || {}; - let type = Reflect.getMetadata("design:paramtypes", target, key)[ - index - ] as OptionValueType; - - if (isParams) { - finalParams = typeOrParams as OptionParams; - } else if (typeOrParams !== undefined) { - type = typeOrParams as OptionValueType; - } + const type = + params?.type ?? + (Reflect.getMetadata("design:paramtypes", target, key)[ + index + ] as OptionValueType); const option = DOption.create( name || key, type, - finalParams?.description, - finalParams?.required, + params?.description, + params?.required, index - ).decorate( - target.constructor, - key, - target[key], - target.constructor, - index - ); + ).decorate(target.constructor, key, target[key], target.constructor, index); option.isNode = true; MetadataStorage.instance.addModifier( - Modifier.create(async (original) => { - original.options = [ - ...original.options, - option - ]; + Modifier.create((original) => { + original.options = [...original.options, option]; }, DSlash).decorate( target.constructor, key, diff --git a/src/decorators/decorators/Permission.ts b/src/decorators/decorators/Permission.ts index 5dbc2f7f..dbb602c0 100644 --- a/src/decorators/decorators/Permission.ts +++ b/src/decorators/decorators/Permission.ts @@ -1,31 +1,28 @@ -import { MetadataStorage, Modifier, PermissionType } from "../.."; +import { ApplicationCommandPermissionData } from "discord.js"; +import { MetadataStorage, Modifier } from "../.."; +import { DCommand } from "../classes/DCommand"; import { DDiscord } from "../classes/DDiscord"; import { DSlash } from "../classes/DSlash"; -export function Permission(id: string, type: PermissionType); -export function Permission(id: string, type: PermissionType) { - const permission = { - id, - type - }; - - return async ( - target: Object, - key: string, - descriptor: PropertyDescriptor - ) => { +export function Permission(permission: ApplicationCommandPermissionData); +export function Permission(...permission: ApplicationCommandPermissionData[]); +export function Permission(...permission: ApplicationCommandPermissionData[]) { + return (target: Object, key: string, descriptor: PropertyDescriptor) => { MetadataStorage.instance.addModifier( - Modifier.create(async (original) => { - original.defaultPermission = false; - original.permissions = [...original.permissions, permission]; + Modifier.create( + (original) => { + original.permissions = [...original.permissions, ...permission]; - if (original instanceof DDiscord) { - original.slashes.map((slash) => { - slash.defaultPermission = false; - slash.permissions = [...slash.permissions, permission]; - }); - } - }, DSlash, DDiscord).decorateUnknown(target, key, descriptor) + if (original instanceof DDiscord) { + [...original.slashes, ...original.commands].forEach((obj) => { + obj.permissions = [...obj.permissions, ...permission]; + }); + } + }, + DSlash, + DCommand, + DDiscord + ).decorateUnknown(target, key, descriptor) ); }; } diff --git a/src/decorators/decorators/SelectMenu.ts b/src/decorators/decorators/SelectMenu.ts new file mode 100644 index 00000000..7e52b5d1 --- /dev/null +++ b/src/decorators/decorators/SelectMenu.ts @@ -0,0 +1,16 @@ +import { MetadataStorage, DSelectMenu } from "../.."; + +export function SelectMenu( + id: string, + params?: { guilds?: string[]; botIds?: [] } +) { + return (target: Object, key: string) => { + const button = DSelectMenu.create( + id, + params?.guilds, + params?.botIds + ).decorate(target.constructor, key, target[key]); + + MetadataStorage.instance.addSelectMenu(button); + }; +} diff --git a/src/decorators/decorators/Slash.ts b/src/decorators/decorators/Slash.ts index ddc99c57..60327224 100644 --- a/src/decorators/decorators/Slash.ts +++ b/src/decorators/decorators/Slash.ts @@ -4,19 +4,17 @@ export function Slash(); export function Slash(name: string); export function Slash(name: string, params: SlashParams); export function Slash(name?: string, params?: SlashParams) { - return async ( - target: Object, - key: string, - descriptor: PropertyDescriptor - ) => { + return (target: Object, key: string) => { name = name || key; name = name.toLocaleLowerCase(); - const slash = DSlash.create(name, params?.description, params?.defaultPermission, params?.guilds).decorate( - target.constructor, - key, - target[key] - ); + const slash = DSlash.create( + name, + params?.description, + params?.defaultPermission, + params?.guilds, + params?.botIds + ).decorate(target.constructor, key, target[key]); MetadataStorage.instance.addSlash(slash); }; diff --git a/src/decorators/index.ts b/src/decorators/index.ts index fe7d9b17..ab0e0dfa 100644 --- a/src/decorators/index.ts +++ b/src/decorators/index.ts @@ -8,14 +8,22 @@ export * from "./decorators/Slash"; export * from "./decorators/Option"; export * from "./decorators/Choice"; export * from "./decorators/Choices"; +export * from "./decorators/DefaultPermission"; export * from "./decorators/Permission"; export * from "./decorators/Group"; export * from "./decorators/Guild"; +export * from "./decorators/Bot"; +export * from "./decorators/Button"; +export * from "./decorators/SelectMenu"; +export * from "./decorators/Command"; +export * from "./decorators/CommandOption"; export * from "./params/DiscordParams"; export * from "./params/OptionParams"; export * from "./params/SlashParams"; +export * from "./classes/DSelectMenu"; +export * from "./classes/DButton"; export * from "./classes/Decorator"; export * from "./classes/DGuard"; export * from "./classes/DDiscord"; diff --git a/src/decorators/params/CommandParams.ts b/src/decorators/params/CommandParams.ts new file mode 100644 index 00000000..d0179ea8 --- /dev/null +++ b/src/decorators/params/CommandParams.ts @@ -0,0 +1,8 @@ +export interface CommandParams { + argSplitter?: string; + description?: string; + directMessage?: boolean; + defaultPermission?: boolean; + guilds?: string[]; + botIds?: string[]; +} diff --git a/src/decorators/params/EventParams.ts b/src/decorators/params/EventParams.ts new file mode 100644 index 00000000..5ac608d2 --- /dev/null +++ b/src/decorators/params/EventParams.ts @@ -0,0 +1,3 @@ +export interface EventParams { + botIds?: string[]; +} diff --git a/src/decorators/params/OptionParams.ts b/src/decorators/params/OptionParams.ts index d0459e78..5ae4c12e 100644 --- a/src/decorators/params/OptionParams.ts +++ b/src/decorators/params/OptionParams.ts @@ -1,4 +1,7 @@ +import { OptionType } from "../.."; + export interface OptionParams { + type?: OptionType; description?: string; required?: boolean; } diff --git a/src/decorators/params/SlashParams.ts b/src/decorators/params/SlashParams.ts index 3bff7a82..ae5f2fcc 100644 --- a/src/decorators/params/SlashParams.ts +++ b/src/decorators/params/SlashParams.ts @@ -2,4 +2,5 @@ export interface SlashParams { description?: string; defaultPermission?: boolean; guilds?: string[]; + botIds?: string[]; } diff --git a/src/logic/index.ts b/src/logic/index.ts index 740afa2b..de8c62fb 100644 --- a/src/logic/index.ts +++ b/src/logic/index.ts @@ -1,5 +1,3 @@ export * from "./metadatas/MetadataStorage"; - export * from "./utils/DecoratorUtils"; - export * from "./di/DIService"; diff --git a/src/logic/metadatas/MetadataStorage.ts b/src/logic/metadatas/MetadataStorage.ts index db151127..d8754b28 100644 --- a/src/logic/metadatas/MetadataStorage.ts +++ b/src/logic/metadatas/MetadataStorage.ts @@ -9,19 +9,29 @@ import { DIService, DSlash, DOption, - Method + Method, } from "../.."; -import { DGroup } from "../../decorators"; +import { DButton, DGroup } from "../../decorators"; +import { DSelectMenu } from "../../decorators/classes/DSelectMenu"; +import * as glob from "glob"; +import { DCommand } from "../../decorators/classes/DCommand"; +import { DCommandOption } from "../../decorators/classes/DCommandOption"; export class MetadataStorage { + private static isBuilt = false; + private static _classesToLoad: string[] = []; private static _instance: MetadataStorage; private _events: DOn[] = []; private _guards: DGuard[] = []; private _slashes: DSlash[] = []; private _allSlashes: DSlash[] = []; + private _buttons: DButton[] = []; + private _selectMenu: DSelectMenu[] = []; private _options: DOption[] = []; private _discords: DDiscord[] = []; private _modifiers: Modifier[] = []; + private _commands: DCommand[] = []; + private _commandsOptions: DCommandOption[] = []; private _groups: DGroup[] = []; private _subGroups: DGroup[] = []; @@ -45,21 +55,24 @@ export class MetadataStorage { * Get the list of used events without duplications */ get usedEvents() { - return this.events.reduce( - (prev, event, index) => { - const found = this.events.find( - (event2) => event.event === event2.event - ); - const foundIndex = this.events.indexOf(found); - - if (foundIndex === index || found.once !== event.once) { - prev.push(event); - } + return this.events.reduce((prev, event, index) => { + const found = this.events.find((event2) => event.event === event2.event); + const foundIndex = found ? this.events.indexOf(found) : -1; + + if (foundIndex === index || found?.once !== event.once) { + prev.push(event); + } - return prev; - }, - [] - ) as readonly DOn[]; + return prev; + }, []) as readonly DOn[]; + } + + static get classes() { + return this._classesToLoad; + } + + static set classes(files: string[]) { + this._classesToLoad = files; } get discords() { @@ -70,6 +83,18 @@ export class MetadataStorage { return this._slashes as readonly DSlash[]; } + get commands() { + return this._commands as readonly DCommand[]; + } + + get buttons() { + return this._buttons as readonly DButton[]; + } + + get selectMenus() { + return this._selectMenu as readonly DSelectMenu[]; + } + get allSlashes() { return this._allSlashes as readonly DSlash[]; } @@ -85,7 +110,10 @@ export class MetadataStorage { private get discordMembers(): readonly Method[] { return [ ...this._slashes, + ...this._commands, ...this._events, + ...this._buttons, + ...this._selectMenu, ]; } @@ -101,6 +129,22 @@ export class MetadataStorage { this._slashes.push(slash); } + addCommand(cmd: DCommand) { + this._commands.push(cmd); + } + + addCommandOption(cmdOption: DCommandOption) { + this._commandsOptions.push(cmdOption); + } + + addButton(button: DButton) { + this._buttons.push(button); + } + + addSelectMenu(selectMenu: DSelectMenu) { + this._selectMenu.push(selectMenu); + } + addOption(option: DOption) { this._options.push(option); } @@ -123,14 +167,37 @@ export class MetadataStorage { DIService.instance.addService(discord.classRef); } + private loadClasses() { + // collect all import paths + const imports: string[] = []; + MetadataStorage.classes.forEach((path) => { + const files = glob.sync(path).filter((file) => typeof file === "string"); + files.forEach((file) => { + if (!imports.includes(file)) imports.push(file); + }); + }); + + // import all files + imports.forEach((file) => require(file)); + } + async build() { + // build the instance if not already built + if (MetadataStorage.isBuilt) return; + MetadataStorage.isBuilt = true; + + // load the classes + this.loadClasses(); + // Link the events with @Discord class instances - this.discordMembers.filter((member) => { + this.discordMembers.forEach((member) => { // Find the linked @Discord of an event const discord = this._discords.find((instance) => { return instance.from === member.from; }); + if (!discord) return; + // You can get the @Discord that wrap a @Command/@On by using // on.discord or slash.discord member.discord = discord; @@ -139,20 +206,38 @@ export class MetadataStorage { discord.slashes.push(member); } + if (member instanceof DCommand) { + discord.commands.push(member); + } + if (member instanceof DOn) { discord.events.push(member); } + + if (member instanceof DButton) { + discord.buttons.push(member); + } + + if (member instanceof DSelectMenu) { + discord.selectMenus.push(member); + } }); - + await Modifier.applyFromModifierListToList(this._modifiers, this._discords); await Modifier.applyFromModifierListToList(this._modifiers, this._events); await Modifier.applyFromModifierListToList(this._modifiers, this._slashes); + await Modifier.applyFromModifierListToList(this._modifiers, this._commands); + await Modifier.applyFromModifierListToList(this._modifiers, this._buttons); await Modifier.applyFromModifierListToList(this._modifiers, this._options); + await Modifier.applyFromModifierListToList( + this._modifiers, + this._selectMenu + ); // Set the class level "group" property of all @Slash // Cannot achieve it using modifiers - this._groups.map((group) => { - this._slashes.map((slash) => { + this._groups.forEach((group) => { + this._slashes.forEach((slash) => { if (group.from !== slash.from) { return; } @@ -176,25 +261,30 @@ export class MetadataStorage { // ...comands // ] // - this._groups.map((group) => { + this._groups.forEach((group) => { const slashParent = DSlash.create( group.name, - group.infos.description - ).decorate( - group.classRef, - group.key, - group.method - ); - - slashParent.discord = this._discords.find((instance) => { + group.infos?.description + ).decorate(group.classRef, group.key, group.method); + + const discord = this._discords.find((instance) => { return instance.from === slashParent.from; }); + if (!discord) return; + + slashParent.discord = discord; + slashParent.guilds = [ ...Client.slashGuilds, - ...slashParent.discord.guilds + ...slashParent.discord.guilds, ]; - slashParent.permissions = slashParent.discord.permissions; + slashParent.botIds = [...slashParent.discord.botIds]; + slashParent.permissions = [ + ...slashParent.permissions, + ...slashParent.discord.permissions, + ]; + slashParent.defaultPermission = slashParent.discord.defaultPermission; groupedSlashes.set(group.name, slashParent); @@ -202,7 +292,7 @@ export class MetadataStorage { return slash.group === slashParent.name && !slash.subgroup; }); - slashes.map((slash) => { + slashes.forEach((slash) => { slashParent.options.push(slash.toSubCommand()); }); }); @@ -221,16 +311,12 @@ export class MetadataStorage { // ] // } // ] - this._subGroups.map((subGroup) => { + this._subGroups.forEach((subGroup) => { const option = DOption.create( subGroup.name, "SUB_COMMAND_GROUP", - subGroup.infos.description - ).decorate( - subGroup.classRef, - subGroup.key, - subGroup.method - ); + subGroup.infos?.description + ).decorate(subGroup.classRef, subGroup.key, subGroup.method); // Get the slashes that are in this subgroup const slashes = this._slashes.filter((slash) => { @@ -259,12 +345,14 @@ export class MetadataStorage { // } // ] // - slashes.map((slash) => { + slashes.forEach((slash) => { option.options.push(slash.toSubCommand()); }); - + // The the root option to the root Slash command - const groupSlash = groupedSlashes.get(slashes[0].group); + const groupSlash = slashes?.[0].group + ? groupedSlashes.get(slashes[0].group) + : undefined; if (groupSlash) { groupSlash.options.push(option); } @@ -272,7 +360,7 @@ export class MetadataStorage { return [ ...this._slashes.filter((s) => !s.group && !s.subgroup), - ...Array.from(groupedSlashes.values()) + ...Array.from(groupedSlashes.values()), ]; } @@ -285,19 +373,18 @@ export class MetadataStorage { trigger( event: Event, client: Client, - once: boolean = false - ): ((...params: ArgsOf) => Promise) { + once = false + ): (...params: ArgsOf) => Promise { const responses: any[] = []; const eventsToExecute = this._events.filter((on) => { - return ( - on.event === event && - on.once === once - ); + return on.event === event && on.once === once; }); return async (...params: ArgsOf) => { for (const on of eventsToExecute) { + const botIDs = on.botIds; + if (botIDs.length && !botIDs.includes(client.botId)) continue; const res = await on.execute(params, client); responses.push(res); } diff --git a/src/logic/utils/DecoratorUtils.ts b/src/logic/utils/DecoratorUtils.ts index 84beed83..0f7e4365 100644 --- a/src/logic/utils/DecoratorUtils.ts +++ b/src/logic/utils/DecoratorUtils.ts @@ -3,16 +3,16 @@ import { Decorator } from "../.."; export class DecoratorUtils { /** * Get the list of the linked decorators - * + * * A and B are two linked decorators - * + * * @example * ```typescript * .@A() * .@B() * method() {} * ``` - * + * * @example * ```typescript * method( @@ -21,22 +21,19 @@ export class DecoratorUtils { * param: string * ) {} * ``` - * + * * @example * ```typescript * .@A() * .@B() * class X {} * ``` - * + * * @param a The decorator * @param list The list of linked decorators to a - * @returns + * @returns */ - static getLinkedObjects( - a: Decorator, - list: Type[] - ) { + static getLinkedObjects(a: Decorator, list: Type[]) { return list.filter((b) => { let cond = a.from === b.from && a.key === b.key; diff --git a/src/types/core/ClientOptions.ts b/src/types/core/ClientOptions.ts index a2a7689d..89f067d9 100644 --- a/src/types/core/ClientOptions.ts +++ b/src/types/core/ClientOptions.ts @@ -1,8 +1,17 @@ -import { ClientOptions as DiscordJSClientOptions } from "discord.js"; +import { ClientOptions as DiscordJSClientOptions, Message } from "discord.js"; import { GuardFunction } from "../public/GuardFunction"; -import { LoadClass } from "./LoadClass"; export interface ClientOptions extends DiscordJSClientOptions { + /** + * Specifiy bot id (added for multiple bot support) + */ + botId?: string; + + /** + * bot prefix resolver + */ + prefix?: string | ((message: Message) => Promise); + /** * Do not log anything in the console */ @@ -11,7 +20,7 @@ export interface ClientOptions extends DiscordJSClientOptions { /** * The classes to load for your discord bot */ - classes?: LoadClass[]; + classes?: string[]; /** * The global guards diff --git a/src/types/core/LoadClass.ts b/src/types/core/LoadClass.ts deleted file mode 100644 index bdf4fb58..00000000 --- a/src/types/core/LoadClass.ts +++ /dev/null @@ -1 +0,0 @@ -export type LoadClass = string | Function; diff --git a/src/types/index.ts b/src/types/index.ts index e01b2037..1a651f52 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,4 @@ export * from "./core/DiscordEvents"; -export * from "./core/LoadClass"; export * from "./core/InstanceOf"; export * from "./core/ClientOptions"; export * from "./core/Next"; @@ -8,5 +7,5 @@ export * from "./core/ChoicesType"; export * from "./public/GuardFunction"; export * from "./public/CommandType"; +export * from "./public/CommandMessage"; export * from "./public/ArgsOf"; -export * from "./public/PermissionType"; diff --git a/src/types/public/CommandMessage.ts b/src/types/public/CommandMessage.ts new file mode 100644 index 00000000..5f1c57f4 --- /dev/null +++ b/src/types/public/CommandMessage.ts @@ -0,0 +1,11 @@ +import { Message } from "discord.js"; +import { DCommand } from "../../decorators/classes/DCommand"; + +export interface CommandMessage extends Message { + command: { + prefix: string; + object: DCommand; + name: string; + argString: string; + }; +} diff --git a/src/types/public/CommandType.ts b/src/types/public/CommandType.ts index 0222eeb9..be356ff2 100644 --- a/src/types/public/CommandType.ts +++ b/src/types/public/CommandType.ts @@ -1,8 +1,15 @@ -import { Channel, ClientUser, Role, TextChannel, User, VoiceChannel } from "discord.js"; +import { + Channel, + ClientUser, + Role, + TextChannel, + User, + VoiceChannel, +} from "discord.js"; export type SubCommand = { - [key: string]: string -} + [key: string]: string; +}; export type StringOptionType = | "STRING" @@ -27,9 +34,7 @@ export enum OptionType { SUB_COMMAND_GROUP = "SUB_COMMAND_GROUP", } -export type StringSubType = - | "SUB_COMMAND" - | "SUB_COMMAND_GROUP"; +export type StringSubType = "SUB_COMMAND" | "SUB_COMMAND_GROUP"; export enum SubType { SUB_COMMAND = "SUB_COMMAND", diff --git a/src/types/public/GuardFunction.ts b/src/types/public/GuardFunction.ts index 409f431f..34a1b2d4 100644 --- a/src/types/public/GuardFunction.ts +++ b/src/types/public/GuardFunction.ts @@ -1,4 +1,8 @@ import { Client, Next } from "../.."; -export type GuardFunction = (params: Type, client: Client, next: Next, datas: DatasType) => any; - +export type GuardFunction = ( + params: Type, + client: Client, + next: Next, + datas: DatasType +) => any; diff --git a/src/types/public/PermissionType.ts b/src/types/public/PermissionType.ts deleted file mode 100644 index 7ad3164d..00000000 --- a/src/types/public/PermissionType.ts +++ /dev/null @@ -1 +0,0 @@ -export type PermissionType = "USER" | "ROLE"; diff --git a/tests/create-on-events.test.ts b/tests/create-on-events.test.ts index 32a350ed..af1a2f42 100644 --- a/tests/create-on-events.test.ts +++ b/tests/create-on-events.test.ts @@ -1,4 +1,5 @@ -import { Discord, On, Client, Guard, GuardFunction, Description } from "../src"; +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Discord, On, Client, Guard, GuardFunction } from "../src"; const guard1: GuardFunction = async ([message]: [string], client, next, mwDatas) => { @@ -9,28 +10,24 @@ const guard1: GuardFunction = } }; -const guard2: GuardFunction = async ( - [message]: [string], - client, - next, - mwDatas -) => { - if (mwDatas.original === "hello0") { - mwDatas.message += "1"; - await next(); - } else { - mwDatas.message += "2"; - } -}; +const guard2: GuardFunction<[string], { original: string; message: string }> = + async ([message]: [string], client, next, mwDatas) => { + if (mwDatas.original === "hello0") { + mwDatas.message += "1"; + await next(); + } else { + mwDatas.message += "2"; + } + }; @Discord() abstract class Bot { - @On("message") + @On("messageCreate") private onMessage([message]: [string]) { return message; } - @On("message") + @On("messageCreate") private onMessage2([message]: [string]) { return message; } @@ -54,7 +51,7 @@ beforeAll(async () => { describe("Create on event", () => { it("Should create and execute two messages events", async () => { - const res = await client.trigger("message", "test"); + const res = await client.trigger("messageCreate", "test"); expect(res).toEqual(["test", "test"]); }); diff --git a/tests/slash.test.ts b/tests/slash.test.ts index 792bef25..b4b5f3d5 100644 --- a/tests/slash.test.ts +++ b/tests/slash.test.ts @@ -1,4 +1,11 @@ -import { Channel, CommandInteraction, CommandInteractionOption, Role, TextChannel, User, VoiceChannel } from "discord.js"; +import { + Channel, + CommandInteraction, + Role, + TextChannel, + User, + VoiceChannel, +} from "discord.js"; import { Discord, Slash, @@ -7,30 +14,23 @@ import { Group, Choices, Client, - DSlash, - DOption, Permission, - OptionValueType, StringOptionType, Guard, - Description + Description, } from "../src"; enum TextChoices { Hello = "Hello", - "Good Bye" = "GoodBye" + "Good Bye" = "GoodBye", } @Discord() @Guild("693401527494377482") -@Group( - "testing", - "Testing group description", - { - maths: "maths group description", - text: "text group description" - } -) +@Group("testing", "Testing group description", { + maths: "maths group description", + text: "text group description", +}) @Guard(async (params, client, next, datas) => { datas.passed = true; return await next(); @@ -48,12 +48,7 @@ export abstract class AppDiscord { client: Client, datas: any ) { - return [ - "/testing maths add", - x + y, - interaction, - datas.passed - ]; + return ["/testing maths add", x + y, interaction, datas.passed]; } @Slash("multiply", { description: "Multiply" }) @@ -67,12 +62,7 @@ export abstract class AppDiscord { client: Client, datas: any ) { - return [ - "/testing maths multiply", - x * y, - interaction, - datas.passed - ]; + return ["/testing maths multiply", x * y, interaction, datas.passed]; } @Slash("hello") @@ -85,12 +75,7 @@ export abstract class AppDiscord { client: Client, datas: any ) { - return [ - "/testing text hello", - text, - interaction, - datas.passed - ]; + return ["/testing text hello", text, interaction, datas.passed]; } @Slash("hello") @@ -103,13 +88,7 @@ export abstract class AppDiscord { client: Client, datas: any ) { - return [ - "/testing hello text", - text, - text2, - interaction, - datas.passed - ]; + return ["/testing hello text", text, text2, interaction, datas.passed]; } } @@ -121,7 +100,7 @@ export abstract class AppDiscord { }) export abstract class AppDiscord1 { @Slash("hello") - @Permission("123", "USER") + @Permission({ id: "123", type: "USER", permission: true }) add( @Option("text", { required: false }) text: string, @@ -129,12 +108,7 @@ export abstract class AppDiscord1 { client: Client, datas: any ) { - return [ - "/hello", - text, - interaction, - datas.passed - ]; + return ["/hello", text, interaction, datas.passed]; } @Slash("inferance") @@ -167,12 +141,7 @@ export abstract class AppDiscord1 { client: Client, datas: any ) { - return [ - "/inferance", - "infer", - interaction, - datas.passed - ]; + return ["/inferance", "infer", interaction, datas.passed]; } } @@ -192,7 +161,7 @@ class FakeOption { name: string, type: StringOptionType, value: string | number, - options?: FakeOption[], + options?: FakeOption[] ) { this.type = type; this.name = name; @@ -213,15 +182,26 @@ class FakeInteraction { isCommand() { return true; } + + isButton() { + return false; + } + + isSelectMenu() { + return false; + } } describe("Slash", () => { it("Should create the slash structure", async () => { expect(client.slashes[0].guilds).toEqual(["invalid_id"]); - expect(client.slashes[0].permissions).toEqual([{ - id: "123", - type: "USER", - }]); + expect(client.slashes[0].permissions).toEqual([ + { + id: "123", + type: "USER", + permission: true, + }, + ]); const slashesObjects = client.slashes.map((slash) => slash.toObject()); expect(slashesObjects).toEqual([ @@ -234,13 +214,11 @@ describe("Slash", () => { name: "text", type: "STRING", required: false, - choices: [ - ], - options: [ - ], + choices: [], + options: [], }, ], - defaultPermission: false, + defaultPermission: true, }, { name: "inferance", @@ -251,80 +229,64 @@ describe("Slash", () => { name: "text", type: "STRING", required: true, - choices: [ - ], - options: [ - ], + choices: [], + options: [], }, { description: "bool - BOOLEAN", name: "bool", type: "BOOLEAN", required: true, - choices: [ - ], - options: [ - ], + choices: [], + options: [], }, { description: "nb - INTEGER", name: "nb", type: "INTEGER", required: true, - choices: [ - ], - options: [ - ], + choices: [], + options: [], }, { description: "channel - CHANNEL", name: "channel", type: "CHANNEL", required: true, - choices: [ - ], - options: [ - ], + choices: [], + options: [], }, { description: "textchannel - CHANNEL", name: "textchannel", type: "CHANNEL", required: false, - choices: [ - ], - options: [ - ], + choices: [], + options: [], }, { description: "voicechannel - CHANNEL", name: "voicechannel", type: "CHANNEL", required: false, - choices: [ - ], - options: [ - ], + choices: [], + options: [], }, { description: "user - USER", name: "user", type: "USER", required: false, - choices: [ - ], - options: [ - ], + choices: [], + options: [], }, { description: "role - ROLE", name: "role", type: "ROLE", required: false, - choices: [ - ], - options: [ - ], + choices: [], + options: [], }, ], defaultPermission: true, @@ -337,15 +299,13 @@ describe("Slash", () => { description: "text group description", name: "text", type: "SUB_COMMAND_GROUP", - choices: [ - ], + choices: [], options: [ { description: "hello", name: "hello", type: "SUB_COMMAND", - choices: [ - ], + choices: [], options: [ { description: "text - STRING", @@ -362,8 +322,7 @@ describe("Slash", () => { value: "GoodBye", }, ], - options: [ - ], + options: [], }, ], }, @@ -373,35 +332,29 @@ describe("Slash", () => { description: "maths group description", name: "maths", type: "SUB_COMMAND_GROUP", - choices: [ - ], + choices: [], options: [ { description: "Multiply", name: "multiply", type: "SUB_COMMAND", - choices: [ - ], + choices: [], options: [ { description: "x value", name: "x", type: "INTEGER", required: false, - choices: [ - ], - options: [ - ], + choices: [], + options: [], }, { description: "y value", name: "y", type: "INTEGER", required: false, - choices: [ - ], - options: [ - ], + choices: [], + options: [], }, ], }, @@ -409,28 +362,23 @@ describe("Slash", () => { description: "Addition", name: "add", type: "SUB_COMMAND", - choices: [ - ], + choices: [], options: [ { description: "x value", name: "x", type: "INTEGER", required: false, - choices: [ - ], - options: [ - ], + choices: [], + options: [], }, { description: "y value", name: "y", type: "INTEGER", required: false, - choices: [ - ], - options: [ - ], + choices: [], + options: [], }, ], }, @@ -440,28 +388,23 @@ describe("Slash", () => { description: "hello", name: "hello", type: "SUB_COMMAND", - choices: [ - ], + choices: [], options: [ { description: "text - STRING", name: "text", type: "STRING", required: true, - choices: [ - ], - options: [ - ], + choices: [], + options: [], }, { description: "text2 - STRING", name: "text2", type: "STRING", required: false, - choices: [ - ], - options: [ - ], + choices: [], + options: [], }, ], }, @@ -472,107 +415,96 @@ describe("Slash", () => { }); it("Should execute the simple slash", async () => { - const interaction = new FakeInteraction( - "hello", - [ - new FakeOption("text", "STRING", "hello") - ] - ); - - const res = await client.executeSlash(interaction as any); + const interaction = new FakeInteraction("hello", [ + new FakeOption("text", "STRING", "hello"), + ]); + + const res = await client.executeInteraction(interaction as any); expect(res).toEqual(["/hello", "hello", interaction, true]); }); it("Should execute the simple grouped text slash", async () => { - const interaction = new FakeInteraction( - "testing", - [ - new FakeOption("hello", "SUB_COMMAND", "text", [ - new FakeOption("text", "STRING", "testing hello text"), - new FakeOption("text2", "STRING", "testing hello text2") - ]) - ] - ); - - const res = await client.executeSlash(interaction as any); - expect(res).toEqual(["/testing hello text", "testing hello text", "testing hello text2", interaction, true]); + const interaction = new FakeInteraction("testing", [ + new FakeOption("hello", "SUB_COMMAND", "text", [ + new FakeOption("text", "STRING", "testing hello text"), + new FakeOption("text2", "STRING", "testing hello text2"), + ]), + ]); + + const res = await client.executeInteraction(interaction as any); + expect(res).toEqual([ + "/testing hello text", + "testing hello text", + "testing hello text2", + interaction, + true, + ]); }); it("Should execute the simple subgrouped text slash", async () => { - const interaction = new FakeInteraction( - "testing", - [ - new FakeOption("text", "SUB_COMMAND_GROUP", "text", [ - new FakeOption("hello", "SUB_COMMAND", "text", [ - new FakeOption("text", "STRING", "testing text hello") - ]) - ]) - ] - ); - - const res = await client.executeSlash(interaction as any); - expect(res).toEqual(["/testing text hello", "testing text hello", interaction, true]); + const interaction = new FakeInteraction("testing", [ + new FakeOption("text", "SUB_COMMAND_GROUP", "text", [ + new FakeOption("hello", "SUB_COMMAND", "text", [ + new FakeOption("text", "STRING", "testing text hello"), + ]), + ]), + ]); + + const res = await client.executeInteraction(interaction as any); + expect(res).toEqual([ + "/testing text hello", + "testing text hello", + interaction, + true, + ]); }); it("Should execute the simple subgrouped multiply slash", async () => { - const interaction = new FakeInteraction( - "testing", - [ - new FakeOption("maths", "SUB_COMMAND_GROUP", "text", [ - new FakeOption("multiply", "SUB_COMMAND", "text", [ - new FakeOption("x", "INTEGER", 2), - new FakeOption("y", "INTEGER", 5) - ]) - ]) - ] - ); - - const res = await client.executeSlash(interaction as any); + const interaction = new FakeInteraction("testing", [ + new FakeOption("maths", "SUB_COMMAND_GROUP", "text", [ + new FakeOption("multiply", "SUB_COMMAND", "text", [ + new FakeOption("x", "INTEGER", 2), + new FakeOption("y", "INTEGER", 5), + ]), + ]), + ]); + + const res = await client.executeInteraction(interaction as any); expect(res).toEqual(["/testing maths multiply", 10, interaction, true]); }); it("Should execute the simple subgrouped addition slash", async () => { - const interaction = new FakeInteraction( - "testing", - [ - new FakeOption("maths", "SUB_COMMAND_GROUP", "text", [ - new FakeOption("add", "SUB_COMMAND", "text", [ - new FakeOption("x", "INTEGER", 2), - new FakeOption("y", "INTEGER", 5) - ]) - ]) - ] - ); - - const res = await client.executeSlash(interaction as any); + const interaction = new FakeInteraction("testing", [ + new FakeOption("maths", "SUB_COMMAND_GROUP", "text", [ + new FakeOption("add", "SUB_COMMAND", "text", [ + new FakeOption("x", "INTEGER", 2), + new FakeOption("y", "INTEGER", 5), + ]), + ]), + ]); + + const res = await client.executeInteraction(interaction as any); expect(res).toEqual(["/testing maths add", 7, interaction, true]); }); it("Should execute the with optional option", async () => { - const interaction = new FakeInteraction( - "hello", - [ - ] - ); + const interaction = new FakeInteraction("hello", []); - const res = await client.executeSlash(interaction as any); + const res = await client.executeInteraction(interaction as any); expect(res).toEqual(["/hello", undefined, interaction, true]); }); it("Should not execute not found slash", async () => { - const interaction = new FakeInteraction( - "testing", - [ - new FakeOption("maths", "SUB_COMMAND_GROUP", "text", [ - new FakeOption("notfound", "SUB_COMMAND", "text", [ - new FakeOption("x", "INTEGER", 2), - new FakeOption("y", "INTEGER", 5) - ]) - ]) - ] - ); - - const res = await client.executeSlash(interaction as any); + const interaction = new FakeInteraction("testing", [ + new FakeOption("maths", "SUB_COMMAND_GROUP", "text", [ + new FakeOption("notfound", "SUB_COMMAND", "text", [ + new FakeOption("x", "INTEGER", 2), + new FakeOption("y", "INTEGER", 5), + ]), + ]), + ]); + + const res = await client.executeInteraction(interaction as any); expect(res).toEqual(undefined); }); }); diff --git a/tsconfig.json b/tsconfig.json index 78867a43..16bf918b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "target": "es2019", "noImplicitAny": false, "sourceMap": true, + "strict": true /* Enable all strict type-checking options. */, "outDir": "build", "emitDecoratorMetadata": true, "experimentalDecorators": true,