├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── doc ├── Application.md ├── Command.md ├── ContextMenu.md ├── Deployment.md ├── Environment.md ├── Event.md ├── Hook.md ├── Starting.md ├── Structure.md ├── advanced │ └── CreateAddon.md ├── exemples │ ├── Buttons.md │ ├── PingPong.md │ └── PresenceProvider.md └── partials │ ├── Command.md │ ├── ContextMenu.md │ └── Event.md ├── package.json ├── src ├── Application.ts ├── Container.ts ├── Environment.ts ├── EnvironmentBuilder.ts ├── Factory.ts ├── Ignitor.ts ├── cli │ ├── help.ts │ └── version.ts ├── entities │ ├── Addon.ts │ ├── Cli.ts │ ├── Command.ts │ ├── ContextMenu.ts │ ├── Event.ts │ ├── Hook.ts │ └── Provider.ts ├── events │ ├── GuildMemberAddBoost.ts │ ├── GuildMemberRemoveBoost.ts │ ├── VoiceJoin.ts │ ├── VoiceLeave.ts │ └── WebsocketDebug.ts ├── index.ts ├── managers │ ├── AddonManager.ts │ ├── BaseCommandManager.ts │ ├── CliManager.ts │ ├── DiscordEventManager.ts │ ├── EventManager.ts │ ├── HookManager.ts │ ├── ProviderManager.ts │ └── commands │ │ ├── ContextMenuCommandManager.ts │ │ └── SlashCommandManager.ts ├── types │ └── index.ts └── utils │ ├── ConsoleColors.ts │ ├── Constructable.ts │ ├── Cooldown.ts │ ├── EntityFile.ts │ ├── NodeEmitter.ts │ └── index.ts ├── test └── index.spec.ts ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | build 3 | node_modules 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": 12, 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "@typescript-eslint", 17 | "ava" 18 | ], 19 | "rules": { 20 | "@typescript-eslint/no-unused-vars": "off", 21 | "space-before-function-paren": "off", 22 | "eol-last": "off", 23 | "no-trailing-spaces": "off", 24 | "curly": "off", 25 | "@typescript-eslint/no-inferrable-types": "off", 26 | "new-cap": "off", 27 | "brace-style": "off", 28 | "@typescript-eslint/no-non-null-assertion": "off", 29 | "@typescript-eslint/no-explicit-any": "off", 30 | "@typescript-eslint/ban-types": "off", 31 | "@typescript-eslint/explicit-module-boundary-types": "off" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | 9 | pull_request: 10 | branches: 11 | - main 12 | - master 13 | 14 | jobs: 15 | build: 16 | runs-on: ${{ matrix.os }} 17 | 18 | strategy: 19 | matrix: 20 | node-version: [16.x] 21 | os: [ubuntu-latest, windows-latest, macos-latest] 22 | fail-fast: false 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | 27 | - name: Use Node.js ${{ matrix.node-version }} 28 | uses: actions/setup-node@v1 29 | with: 30 | node-version: ${{ matrix.node-version }} 31 | 32 | - name: Install 33 | run: npm install 34 | 35 | - name: Lint 36 | run: npm run lint --if-present 37 | 38 | - name: Test 39 | run: npm run test --if-present 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # idea 26 | .idea 27 | 28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | oldsrc/typing/ 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional eslint cache 51 | .eslintcache 52 | 53 | # Optional REPL history 54 | .node_repl_history 55 | 56 | # Output of 'npm pack' 57 | *.tgz 58 | 59 | # Yarn Integrity file 60 | .yarn-integrity 61 | 62 | # dotenv environment variables file 63 | .env 64 | 65 | # parcel-bundler cache (https://parceljs.org/) 66 | .cache 67 | 68 | # next.js build output 69 | .next 70 | 71 | # nuxt.js build output 72 | .nuxt 73 | 74 | # Nuxt generate 75 | dist 76 | build 77 | oldsrc 78 | 79 | # vuepress build output 80 | .vuepress/dist 81 | 82 | # Serverless directories 83 | .serverless 84 | 85 | # IDE 86 | .idea 87 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | .idea 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | src/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # TypeScript cache 47 | *.tsbuildinfo 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Microbundle cache 56 | .rpt2_cache/ 57 | .rts2_cache_cjs/ 58 | .rts2_cache_es/ 59 | .rts2_cache_umd/ 60 | 61 | # Optional REPL history 62 | .node_repl_history 63 | 64 | # Output of 'npm pack' 65 | *.tgz 66 | 67 | # Yarn Integrity file 68 | .yarn-integrity 69 | 70 | # dotenv environment variables file 71 | .env 72 | .env.test 73 | 74 | # parcel-bundler cache (https://parceljs.org/) 75 | .cache 76 | 77 | # Next.js build output 78 | .next 79 | 80 | # Nuxt.js build / generate output 81 | .nuxt 82 | dist 83 | 84 | # Gatsby files 85 | .cache/ 86 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 87 | # https://nextjs.org/blog/next-9-1#public-directory-support 88 | # public 89 | 90 | # vuepress build output 91 | .vuepress/dist 92 | 93 | # Serverless directories 94 | .serverless/ 95 | 96 | # FuseBox cache 97 | .fusebox/ 98 | 99 | # DynamoDB Local files 100 | .dynamodb/ 101 | 102 | # TernJS port file 103 | .tern-port -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Anthony Fu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🧡 Core-next 2 | The `@discord-factory/core-next` module is the core project of the Discord Factory framework. 3 | It handles everything related to project initialization, slash command registration within Discord and the instantiation of the Discord client. 4 | 5 | If you want to install the framework, don't just download the `@discord-factory/core-next` package as this fully exploits a previously defined framework. 6 | 7 | 8 | ## License 9 | 10 | [MIT](./LICENSE) License © 2021 [Baptiste Parmantier](https://github.com/LeadcodeDev) 11 | -------------------------------------------------------------------------------- /doc/Application.md: -------------------------------------------------------------------------------- 1 | # Application 2 | 3 | When you develop your application, you will have to import and operate your [discord client](https://discord.js.org/#/docs/main/stable/class/Client). 4 | 5 | For performance reasons, the `client` instance is not transferred to the instance of your files such as events, commands, hooks, etc. 6 | 7 | However, you can still access it via an `entry` class as below : 8 | ```ts 9 | import { Application } from 'ioc:factory/Core' 10 | ``` 11 | 12 | ### Client 13 | ```ts 14 | import { Application } from 'ioc:factory/Core' 15 | import { Client } from 'discord.js' 16 | 17 | const client: Client = Application.getClient() 18 | console.log(client) 19 | ``` 20 | 21 | ### Environment 22 | ```ts 23 | import { Application } from 'ioc:factory/Core' 24 | 25 | const environment: { [p: string]: unknown } = Application.getEnvironment() 26 | console.log(environment) 27 | ``` 28 | 29 | ### Environment 30 | ```ts 31 | import { Application } from 'ioc:factory/Core' 32 | 33 | const key: unknown | undefined = Application.getEnvironmentValue() 34 | console.log(key) 35 | ``` 36 | 37 | ### Events 38 | ```ts 39 | import { Application } from 'ioc:factory/Core' 40 | import { EventEntity } from 'ioc:factory/Core/Provider' 41 | 42 | const commands: EventEntity[] = Application.getEvents() 43 | console.log(commands) 44 | ``` 45 | 46 | ### Commands 47 | ```ts 48 | import { Application } from 'ioc:factory/Core' 49 | import { CommandEntity } from 'ioc:factory/Core/Provider' 50 | 51 | const commands: CommandEntity[] = Application.getCommands() 52 | console.log(commands) 53 | ``` 54 | 55 | ### Context menus 56 | ```ts 57 | import { Application } from 'ioc:factory/Core' 58 | import { ContextMenuEntity } from 'ioc:factory/Core/Provider' 59 | 60 | const contextMenus: ContextMenuEntity[] = Application.getContextMenu() 61 | console.log(contextMenus) 62 | ``` -------------------------------------------------------------------------------- /doc/Command.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | The [version 13 update](https://github.com/discordjs/discord.js/blob/main/CHANGELOG.md#1300-2021-08-06) of [discord.js](https://discord.js.org) marks the arrival of Slash Commands. 3 | This new feature provides real support for developers who want to create commands on their bots. 4 | Discord has announced that it will gradually replace the old ordering system we all knew, based on a prefix as the first character of a message, with this new ordering system. 5 | 6 | ## Create new command from CLI 7 | Open a new terminal in your project and write the following command : 8 | 9 | ```bash 10 | npm run factory make:command 11 | # or 12 | yarn factory make:command 13 | ``` 14 | ::: info 15 | It is important to note that when you define the file name, you can `place` the file in folders by specifying a path directory in addition to the file name as in the following example. 16 | ::: 17 | 18 | ## Default command file 19 | 20 | A file will be created in the specified location otherwise in the root of your project with the following structure : 21 | 22 | ```ts 23 | import { BaseCommand, Command } from 'ioc:factory/Core/Command' 24 | import { CommandInteraction } from 'discord.js' 25 | 26 | @Command({ 27 | scope: ['your guild id'], 👈 // Or 'GLOBAL' or 'GUILDS 28 | options: { 29 | name: 'foo', 30 | description: 'Your foo command description', 31 | options: [], 32 | }, 33 | }) 34 | export default class FooCommand implements BaseCommand { 35 | public async run(interaction: CommandInteraction): Promise { 36 | // Your code here 37 | } 38 | } 39 | 40 | ``` 41 | ## Decorator options 42 | A file will be created in the specified location otherwise in the root of your project with the following structure : 43 | 44 | ```ts 45 | export interface CommandGlobalContext { 46 | scope: 'GLOBAL' | 'GUILDS' | Snowflake[], 👈 // Or 'GLOBAL' if you want to register globally 47 | cooldown?: { 48 | time: number 👈 // Measured in milliseconds 49 | count: number 👈 // Cannot be used without time 50 | } 51 | options: { 52 | name: string, 53 | description: string, 54 | } 55 | } 56 | ``` 57 | The `cooldown` key allows you to define a maximum number of uses during a defined time period for the user who will execute the associated command. 58 | 59 | You can set only the `time` key if you wish to limit the use of the command for a certain period of time. If `count` is not set, the maximum number of times the command can be used for the set time will be `1`. 60 | 61 | ::: warning 62 | The `count` key cannot be used if `time` is not set. 63 | ::: 64 | 65 | See more about ApplicationCommandOption [here](https://discord.js.org/#/docs/main/stable/typedef/ApplicationCommandOption) -------------------------------------------------------------------------------- /doc/ContextMenu.md: -------------------------------------------------------------------------------- 1 | # Context menus 2 | The [version 13 update](https://github.com/discordjs/discord.js/blob/main/CHANGELOG.md#1300-2021-08-06) of [discord.js](https://discord.js.org) marks the arrival of Slash Commands. 3 | This new feature provides real support for developers who want to create commands on their bots. 4 | Discord has announced that it will gradually replace the old ordering system we all knew, based on a prefix as the first character of a message, with this new ordering system. 5 | 6 | ## Create new context menu from CLI 7 | Open a new terminal in your project and write the following command : 8 | 9 | ```bash 10 | npm run factory make:context-menu 11 | # or 12 | yarn factory make:context-menu 13 | ``` 14 | ::: info 15 | It is important to note that when you define the file name, you can `place` the file in folders by specifying a path directory in addition to the file name as in the following example. 16 | ::: 17 | 18 | ## Default context menu file 19 | 20 | A file will be created in the specified location otherwise in the root of your project with the following structure : 21 | 22 | ```ts 23 | import { BaseContextMenu, ContextMenu } from 'ioc:factory/Core/ContextMenu' 24 | import { ContextMenuInteraction } from 'discord.js' 25 | 26 | @ContextMenu({ 27 | scope: ['your guild id'], 👈 // Or 'GLOBAL' or 'GUILDS 28 | options: { 29 | name: 'foo', 30 | type: 'USER' 👈 // Or 'MESSAGE' 31 | }, 32 | }) 33 | export default class FooContextMenu implements BaseContextMenu { 34 | public async run(interaction: ContextMenuInteraction): Promise { 35 | // Your code here 36 | } 37 | } 38 | 39 | ``` 40 | ## Decorator options 41 | A file will be created in the specified location otherwise in the root of your project with the following structure : 42 | 43 | ```ts 44 | export interface ApplicationGlobalContext { 45 | scope: 'GLOBAL' | 'GUILDS' | Snowflake[], 👈 // Or 'GLOBAL' if you want to register globally 46 | permissions?: ApplicationCommandPermissionData[], 47 | cooldown?: { 48 | time: number 👈 // Measured in milliseconds 49 | count: number 👈 // Cannot be used without time 50 | } 51 | options: { 52 | name: string, 53 | type: 'USER' | 'MESSAGE', 54 | } 55 | } 56 | ``` 57 | The `cooldown` key allows you to define a maximum number of uses during a defined time period for the user who will execute the associated command. 58 | 59 | You can set only the `time` key if you wish to limit the use of the command for a certain period of time. If `count` is not set, the maximum number of times the command can be used for the set time will be `1`. 60 | 61 | ::: warning 62 | The `count` key cannot be used if `time` is not set. 63 | ::: -------------------------------------------------------------------------------- /doc/Deployment.md: -------------------------------------------------------------------------------- 1 | # Deployment 2 | Deployment is the logical phase after developing an application. This section will deal with the production deployment of the application. In the first time, you should to install all dependencies. 3 | 4 | ## Starting your project 5 | All you have to do is install the dependencies with the following commands 6 | 7 | ```bash 8 | npm install 9 | # or 10 | yarn install 11 | ``` 12 | 13 | In the second time, you should build your application because the production mode cannot read and execute the typescript natively. 14 | 15 | ```bash 16 | npm run build 17 | # or 18 | yarn build 19 | ``` 20 | 21 | ## Deploy with PM2 22 | PM2 is a daemon process manager that will help you manage and keep your application online 24/7. 23 | 24 | ### Install PM2 : 25 | ```bash 26 | npm install -g pm2 27 | # or 28 | yarn global add pm2 29 | ``` 30 | 31 | Create a root file named `ecosystem.config.js`. 32 | 33 | ###### ecosystem.config.js 34 | ```ts 35 | module.exports = { 36 | apps : [{ 37 | name : 'Discord Factory application', 38 | script : 'npm', 39 | args : 'start' 40 | }] 41 | } 42 | ``` 43 | 44 | ::: info 45 | You can generate the `ecosystem.config.js` file using the following command: 46 | ```bash 47 | yarn factory pm2:ecosystem 48 | # or 49 | npm run factory pm2:ecosystem 50 | ``` 51 | ::: 52 | 53 | Then open a terminal in the root folder of your application and run the following command : 54 | ```bash 55 | cd /path/to/project/folder/root 56 | pm2 start 57 | ``` 58 | 59 | ## Deploy with Docker 60 | Docker is free software for launching applications in isolated containers. 61 | 62 | Learn more in the [Docker Documentation]() 63 | 64 | ::: warning 65 | In the docker section, replace 66 | - [name] with the name of your bot, 67 | - [version] with the version of your bot. (You can use `latest`, `dev`... with docker) 68 | - [env] with your env file (`environment.json` or `environment.yml`) 69 | ::: 70 | 71 | Create a root file named Dockerfile. 72 | 73 | ```dockerfile 74 | FROM node:16-alpine3.11 75 | 76 | RUN mkdir -p /usr/src/[name] 77 | 78 | WORKDIR /usr/src/[name] 79 | 80 | COPY . /usr/src/[name] 81 | 82 | RUN yarn build 83 | 84 | CMD ["yarn", "start"] 85 | ``` 86 | 87 | Then open a terminal in the root folder of your application and run the following command : 88 | 89 | ```bash 90 | cd /path/to/project/folder/root 91 | docker build -t [name]:[version] . 92 | ``` 93 | 94 | Use docker-compose to automate the start command of your bot with the following content : 95 | ```dockerfile 96 | version: "3" 97 | 98 | services: 99 | [name]: 100 | image: [name]:[version] 101 | container_name: [name] 102 | volumes: 103 | - "/path/to/project/folder/root/[env]:/usr/src/[name]/[env]:ro" 104 | ``` 105 | 106 | Finally, you can run your image inside a container with the following command : 107 | 108 | ```bash 109 | docker-compose up -d 110 | ``` 111 | 112 | ::: info 113 | The option -d allows to launch the container in the background. 114 | ::: 115 | 116 | To stop the container, run the following command : 117 | ```bash 118 | docker-compose down 119 | ``` 120 | 121 | To stop see the log of your discord bot, run the following command : 122 | 123 | ```bash 124 | docker logs [name] 125 | ``` -------------------------------------------------------------------------------- /doc/Environment.md: -------------------------------------------------------------------------------- 1 | # 🌲 Environment 2 | 3 | The environment file has an extremely important place in the framework, in fact you can have two different environments, one for development and the other for production. 4 | Each of these two environments is represented by a file on your disk, at the root of your project and can take the extensions `json` or `yaml`. 5 | 6 | ## Development 7 | This mode is used when you are designing your application, it is called `environment.dev.(json|yaml)` 8 | 9 | The structure of the file will be detailed below. 10 | 11 | ## Production 12 | This mode is used when you are deploying your application, it is called `environment.prod.(json|yaml)` 13 | 14 | The structure of the file will be detailed below. 15 | 16 | ## Minimal structure 17 | Whatever extension of your environment you choose, you must respect the following interface 18 | ```ts 19 | type Json = { [K in string]: string } 20 | 21 | interface environment { 22 | APP_TOKEN: string 23 | PARTIALS: string[] 24 | INTENTS: string[] 25 | MY_CUSTOM_KEY: string | string[] | Json 26 | } 27 | ``` 28 | 29 | ### If you have chosen YAML 30 | ###### environment.(dev|prod).yaml 31 | ```yaml 32 | APP_TOKEN: Your token here 33 | PARTIALS: 34 | - MESSAGE 35 | - CHANNEL 36 | - REACTION 37 | INTENTS: 38 | - GUILDS 39 | - GUILD_MEMBERS 40 | - GUILD_BANS 41 | - GUILD_EMOJIS_AND_STICKERS 42 | - GUILD_INTEGRATIONS 43 | - GUILD_WEBHOOKS 44 | - GUILD_INVITES 45 | - GUILD_VOICE_STATES 46 | - GUILD_PRESENCES 47 | - GUILD_MESSAGES 48 | - GUILD_MESSAGE_REACTIONS 49 | - GUILD_MESSAGE_TYPING 50 | - DIRECT_MESSAGES 51 | - DIRECT_MESSAGE_REACTIONS 52 | - DIRECT_MESSAGE_TYPING 53 | 54 | # Other settings.. 55 | ``` 56 | 57 | ### If you have chosen JSON 58 | ###### environment.(dev|prod).json 59 | ```json 60 | { 61 | "APP_TOKEN": "Your token here", 62 | "PARTIALS": ["MESSAGE" , "CHANNEL", "REACTION"], 63 | "INTENTS": [ 64 | "GUILDS", 65 | "GUILD_MEMBERS", 66 | "GUILD_BANS", 67 | "GUILD_EMOJIS_AND_STICKERS", 68 | "GUILD_INTEGRATIONS", 69 | "GUILD_WEBHOOKS", 70 | "GUILD_INVITES", 71 | "GUILD_VOICE_STATES", 72 | "GUILD_PRESENCES", 73 | "GUILD_MESSAGES", 74 | "GUILD_MESSAGE_REACTIONS", 75 | "GUILD_MESSAGE_TYPING", 76 | "DIRECT_MESSAGES", 77 | "DIRECT_MESSAGE_REACTIONS", 78 | "DIRECT_MESSAGE_TYPING" 79 | ] 80 | //Other settings.. 81 | } 82 | ``` -------------------------------------------------------------------------------- /doc/Event.md: -------------------------------------------------------------------------------- 1 | # Event 2 | The design of a discord bot spends most of the time developing commands. The purpose of these commands is to execute certain actions by the moderation or the community. 3 | Creating an event with the framework is very simple. 4 | 5 | ## Create new event from CLI 6 | Open a new terminal in your project and write the following command : 7 | 8 | ```bash 9 | npm run factory make:event 10 | # or 11 | yarn factory make:event 12 | ``` 13 | ::: info 14 | It is important to note that when you define the file name, you can `place` the file in folders by specifying a path directory in addition to the file name as in the following example. 15 | ::: 16 | 17 | ## Default event file 18 | A file will be created in the specified location otherwise in the root of your project with the following structure : 19 | 20 | ```ts 21 | import { Event, BaseEvent } from 'ioc:factory/Core/Event' 22 | import { Message } from 'discord.js' 23 | 24 | @Event('message') 25 | export default class FooEvent implements BaseEvent { 26 | public async run(message: Message): Promise { 27 | // Your code here 28 | } 29 | } 30 | ``` 31 | 32 | ## New exclusive Factory events 33 | 34 | ### Event : websocketDebug 35 | The `websocketDebug` event provides you with payloads received from the Discord API. 36 | ```ts 37 | import { Event, BaseEvent } from 'ioc:factory/Core/Event' 38 | import { Message } from 'discord.js' 39 | 40 | @Event('websocketDebug') 41 | export default class FooEvent implements BaseEvent { 42 | public async run(payload: any): Promise { 43 | // Your code here 44 | } 45 | } 46 | ``` 47 | 48 | ### Event : voiceMemberJoin 49 | The `voiceMemberJoin` event is emitted when a member joins a voice channel. 50 | 51 | ```ts 52 | import { Event, BaseEvent } from 'ioc:factory/Core/Event' 53 | import { VoiceState } from 'discord.js' 54 | 55 | @Event('voiceJoin') 56 | export default class FooEvent implements BaseEvent { 57 | public async run (state: VoiceState): Promise { 58 | // Your code here 59 | } 60 | } 61 | ``` 62 | 63 | ### Event : voiceMemberLeave 64 | The `voiceMemberLeave` event is emitted when a member leaves a voice channel. 65 | 66 | ```ts 67 | import { Event, BaseEvent } from 'ioc:factory/Core/Event' 68 | import { VoiceState } from 'discord.js' 69 | 70 | @Event('voiceLeave') 71 | export default class FooEvent implements BaseEvent { 72 | public async run (state: VoiceState): Promise { 73 | // Your code here 74 | } 75 | } 76 | ``` 77 | 78 | ### Event : guildMemberAddBoost 79 | The `guildMemberAddBoost` event is emitted when a new member boosts the server. 80 | ```ts 81 | import { Event, BaseEvent } from 'ioc:factory/Core/Event' 82 | import { GuildMember } from 'discord.js' 83 | 84 | @Event('guildMemberAddBoost') 85 | export default class FooEvent implements BaseEvent { 86 | public async run(member: GuildMember): Promise { 87 | // Your code here 88 | } 89 | } 90 | ``` 91 | 92 | ### Event : guildMemberRemoveBoost 93 | The `guildMemberRemoveBoost` event is emitted when a member no longer boosts the server. 94 | ```ts 95 | import { Event, BaseEvent } from 'ioc:factory/Core/Event' 96 | import { GuildMember } from 'discord.js' 97 | 98 | @Event('guildMemberRemoveBoost') 99 | export default class FooEvent implements BaseEvent { 100 | public async run(member: GuildMember): Promise { 101 | // Your code here 102 | } 103 | } 104 | ``` -------------------------------------------------------------------------------- /doc/Hook.md: -------------------------------------------------------------------------------- 1 | # ⚓ Hook 2 | When you develop an application with a framework, you are generally blocked by the fact that you cannot interact at certain times during the initialization of the framework... 3 | This is a real problem that is solved by using hooks. Creating a hook with the framework is very simple. 4 | 5 | ## Create new hook from CLI 6 | Open a new terminal in your project and write the following command : 7 | 8 | ```bash 9 | npm run factory make:hook 10 | # or 11 | yarn factory make:hook 12 | ``` 13 | ::: info 14 | It is important to note that when you define the file name, you can `place` the file in folders by specifying a path directory in addition to the file name as in the following example. 15 | ::: 16 | 17 | ## Default hook file 18 | A file will be created in the specified location otherwise in the root of your project with the following structure : 19 | 20 | ```ts 21 | import { Hook, BaseHook } from 'ioc:factory/Core/Hook' 22 | import { Message } from 'discord.js' 23 | 24 | @Hook('hooks') 25 | export default class FooHook implements BaseHook { 26 | public async run(...params: unknown[]): Promise { 27 | // Your code here 28 | } 29 | } 30 | ``` -------------------------------------------------------------------------------- /doc/Starting.md: -------------------------------------------------------------------------------- 1 | # 💪 Starting 2 | 3 | Creating a new project is usually a headache to restore configurations that you would have defined in another application. 4 | 5 | In order to design your first `Discord Factory` project, please use the following command according to your package manager: 6 | ```bash 7 | npm init factory-app 8 | # or 9 | yarn create factory-app 10 | ``` 11 | 12 | That's it ! 13 | 14 | It's up to you ! -------------------------------------------------------------------------------- /doc/Structure.md: -------------------------------------------------------------------------------- 1 | # Structure 2 | The framework offers a very modular way of structuring your files within your application, the only restriction is that they must be included in the `src/` folder as this represents level 0 of your application (also called root directory). 3 | ``` 4 | ├─ node_modules 5 | ├─ contracts 6 | ├─ provider 7 | └─ AppProvider.ts 8 | ├─ src 9 | ├─ start 10 | ├─ index.ts 11 | └─ Kernel.ts 12 | ├─ test 13 | environment.dev.(yaml|json) 14 | environment.prod.(yaml|json) 15 | .eslintignore 16 | .eslintrc 17 | .npmignore 18 | LICENSE 19 | README.md 20 | package.json 21 | tsconfig.json 22 | ``` 23 | 24 | ### Index 25 | This folder contains the files needed to start the application. 26 | You will find the index, the `entry point` of the application which initializes the application. 27 | 28 | ###### App/Start/index.ts 29 | ```ts 30 | import { Ignitor } from '@discord-factory/core-next' 31 | 32 | const ignitor = new Ignitor() 33 | ignitor.createFactory() 34 | ``` 35 | 36 | ::: info 37 | The index.ts file in the start folder is the entry point for your application. 38 | ::: 39 | 40 | ## Kernel 41 | The `Kernel.ts` file is essential to the use of the framework, 42 | this file is initialized in the first ones and allows to inject modules, commands, events or database drivers, etc 43 | 44 | ###### App/Start/Kernel.ts 45 | ```ts 46 | import CoreCommands from '@discord-factory/core-commands' 👈 // Import your module from NPM node_modules 47 | 48 | export default class Kernel { 49 | public registerAddons () { 50 | return [CoreCommands] 👈 // Use your module here, do not instanciate it. 51 | } 52 | } 53 | ``` 54 | 55 | ## Provider 56 | Providers are files that have certain methods defined in advance. 57 | You can create them at will as long as they are built in the following way : 58 | 59 | ###### App/Providers/AppProvider.ts 60 | ```ts 61 | import { BaseProvider, EntityResolvable } from 'ioc:factory/Core/Provider' 62 | import Logger from '@leadcodedev/logger' 63 | 64 | export default class AppProvider implements Provider { 65 | public async boot (): Promise { 66 | Logger.send('info', 'Application start') 67 | // Your code here 68 | } 69 | 70 | public async load (Class: EntityResolvable): Promise { 71 | Logger.send('info', `Load file ${Class.file?.relativePath}`) 72 | // Your code here 73 | } 74 | 75 | public async ok (): Promise { 76 | Logger.send('info', 'Application is ready') 77 | // Your code here 78 | } 79 | } 80 | ``` 81 | 82 | ::: info 83 | You can create as many providers as you like, they will be executed in alphabetical order. 84 | You can learn more here. 85 | ::: 86 | 87 | These files are read first, even before the recovery of the command files, events... 88 | It can be very interesting to use them to record a default behaviour before the application is ready to run. 89 | 90 | ## Src 91 | The `src/` folder is the base folder for your project. 92 | This is where you will work.Please consider this folder as the root of your application. 93 | 94 | The advantage of considering the `src/` folder as the base of your application is that you can structure it as you see fit. 95 | It can be interesting to look at design patterns, here are some of them : 96 | 97 | - [Monolithic Architecture vs Microservice](https://www.geeksforgeeks.org/monolithic-vs-microservices-architecture/) 98 | - [NodeTSkeleton, a clean architecture](https://dev.to/vickodev/nodetskeleton-clean-arquitecture-template-project-for-nodejs-gge) 99 | - [Hexagonal Architecture](https://blog.octo.com/architecture-hexagonale-trois-principes-et-un-exemple-dimplementation) 100 | 101 | Please use the factory make:file command to create a file quickly 102 | 103 | ## Import with alias 104 | The `src/` folder is the base folder for your project. 105 | To simplify the import of your files, the alias `App/` is available. 106 | This alias refers to the root folder `src/`. 107 | 108 | ```ts 109 | - import Foo from '../../../Foo' 110 | + import Foo from 'App/Folder/Foo' 111 | ``` 112 | 113 | ## Testing 114 | It is very important to test your code using unit tests for small features or integration tests for large features. 115 | This folder allows you to write tests on files named `foo.spec.ts`. 116 | The default test framework used in the Discord Factory framework is ava but you can replace it with any other. 117 | 118 | ::: warning 119 | Do not neglect unit or integration testing. 120 | They are extremely useful in the medium/long term. 121 | Indeed, when you develop a new feature, it must not break the existing code, this is called regression. 122 | ::: 123 | 124 | The strict minimum code is as follows : 125 | 126 | ```ts 127 | import test from 'ava' 128 | 129 | test('foo', (t) => { 130 | t.pass() 131 | }) 132 | ``` 133 | 134 | Then you can use the following command to run your tests : 135 | 136 | ```bash 137 | npm install 138 | # or 139 | yarn install 140 | ``` -------------------------------------------------------------------------------- /doc/advanced/CreateAddon.md: -------------------------------------------------------------------------------- 1 | # 🧱 Create your addon 2 | 3 | When you are developing, it happens very regularly that some of your features are found to be common to several projects. 4 | 5 | Normally, you would just copy and paste your "module" and carry it over to the next discord application. 6 | 7 | To address this need for portability, the `Discord Factory` framework allows you to export your functionality to modules registered on the [NPM registry](https://docs.npmjs.com/cli/v7/commands/npm-publish). 8 | 9 | ## Generate a new addon 10 | In order to simplify the design of addons, we provide you with a command to create a clean and healthy base to start your module on a good basis. 11 | ```bash 12 | npm init factory-addon MyAddon 13 | # or 14 | yarn create factory-addon MyAddon 15 | ``` 16 | 17 | ## Structure 18 | ``` 19 | ├─ node_modules 20 | ├─ src 21 | ├─ commands 22 | ├─ types 23 | └─ index.ts 24 | ├─ test 25 | .eslintignore 26 | .eslintrc 27 | .npmignore 28 | LICENSE 29 | README.md 30 | package.json 31 | tsconfig.json 32 | ``` 33 | 34 | ### Index.ts 35 | ```ts 36 | import { BaseAddon } from '@discord-factory/core-next' 37 | 38 | export default class Index extends BaseAddon { 39 | public addonName: string = 'ADDON_NAME' 40 | 41 | /** 42 | * This function is the first to be read within the addons, 43 | * it allows to perform initialization actions 44 | */ 45 | public async init (): Promise { 46 | return this 47 | } 48 | 49 | public registerHooks () { 50 | return [] 51 | } 52 | 53 | public registerCLI () { 54 | return [] 55 | } 56 | 57 | public registerCommands () { 58 | return [] 59 | } 60 | 61 | public registerEvents () { 62 | return [] 63 | } 64 | 65 | public defineKeys () { 66 | return [] 67 | } 68 | } 69 | 70 | /** 71 | * Export your public elements 72 | */ 73 | export { 74 | 75 | } 76 | ``` 77 | 78 | ## Events 79 | Using the `registerEvents()` function, you can inject events into the Factory instance. 80 | To do this, you just have to create a "classic" event that you would create in a "normal" bot but the extended class was replaced by BaseAddonEvent, 81 | then import it __without instantiating it__ into the reto array 82 | 83 | Within the events, commands or context menus that you design through addon design, you will have access to several methods and keys to access the context of your addon. 84 | 85 | ## Commands 86 | Using the `registerCommands()` function, you can inject slash commands into the Factory instance. 87 | To do this, you just have to create a "classic" command that you would create in a "normal" bot but the extended class was replaced by BaseAddonCommand, 88 | then import it __without instantiating it__ into the reto array 89 | 90 | Within the commands, commands or context menus that you design through addon design, you will have access to several methods and keys to access the context of your addon. 91 | 92 | ## CLI 93 | Using the `registerCLI()` function, you can inject CLI commands into the Factory instance. 94 | To do this, you just have to create a new file like below, then import it __without instantiating it__ into the reto array 95 | 96 | ```ts 97 | import { CLI, BaseCli, CliContextRuntime } from '@discord-factory/core-next' 98 | import Addon from '../index' 99 | import Logger from '@leadcodedev/logger' 100 | 101 | @CLI({ 102 | prefix: 'prefix', 103 | description: 'Your command escription', 104 | alias: ['--help', '-h'], 105 | }) 106 | export default class MyCLICommand extends BaseCli { 107 | public async run (context: CliContextRuntime): Promise { 108 | // Your code here 109 | } 110 | } 111 | ``` 112 | 113 | ### CliContextRuntime 114 | ```ts 115 | type NativeResolvable = { [K: string]: string | number | boolean | NativeResolvable } 116 | 117 | export type CliContextRuntime = { 118 | options: NativeResolvable, 119 | args: NativeResolvable, 120 | cli: CAC, 121 | ignitor: Ignitor 122 | } 123 | ``` 124 | 125 | ## Addon context 126 | Within the commands, commands or context menus that you design through addon design, you will have access to several methods and keys to access the context of your addon. 127 | 128 | ```ts 129 | interface context { 130 | addon: Addon 131 | client: Client 132 | getModuleEnvironment (): string 133 | getContainer (): Container 134 | getFiles (): Collection 135 | getSelectEnvironment (): 'yaml' | 'yml' | 'json' 136 | } 137 | ``` 138 | 139 | Once your module is finished, you can save it on the NPM registry and import it into your bot discord application ! -------------------------------------------------------------------------------- /doc/exemples/Buttons.md: -------------------------------------------------------------------------------- 1 | # ❔ How to use buttons 2 | 3 | A concrete example would be the console display of the files instantiated in your application : 4 | 5 | ```bash 6 | npm run factory make:command ButtonCommand 7 | # or 8 | yarn factory make:command ButtonCommand 9 | ``` 10 | 11 | ```ts 12 | import { BaseCommand, Command } from 'ioc:factory/Core/Command' 13 | import { CommandInteraction, MessageActionRow, MessageButton } from 'discord.js' 14 | 15 | @Command({ 16 | scope: 'GUILDS', 17 | options: { 18 | name: 'show-buttons', 19 | description: 'Show buttons command', 20 | options: [], 21 | }, 22 | }) 23 | export default class ButtonCommand implements BaseCommand { 24 | public async run (interaction: CommandInteraction): Promise { 25 | const button = new MessageButton() 26 | .setStyle('SUCCESS') 27 | .setEmoji('✔') 28 | .setLabel('Success') 29 | .setCustomId('unique-button-id') 30 | 31 | const row = new MessageActionRow() 32 | .addComponents(button) 33 | 34 | await interaction.reply({ 35 | content: 'I send my buttons', 36 | components: [row] 37 | }) 38 | } 39 | } 40 | ``` -------------------------------------------------------------------------------- /doc/exemples/PingPong.md: -------------------------------------------------------------------------------- 1 | # Ping pong 2 | 3 | A concrete example would be the console display of the files instantiated in your application : 4 | 5 | ```bash 6 | npm run factory make:command PingPong 7 | # or 8 | yarn factory make:command PingPong 9 | ``` 10 | 11 | ```ts 12 | import { BaseCommand, Command } from 'ioc:factory/Core/Command' 13 | import { CommandInteraction } from 'discord.js' 14 | 15 | @Command({ 16 | scope: 'GUILDS', 17 | options: { 18 | name: 'ping', 19 | description: 'Ping-pong command', 20 | options: [], 21 | }, 22 | }) 23 | export default class PingCommand implements BaseCommand { 24 | public async run(interaction: CommandInteraction): Promise { 25 | await interaction.reply('Pong !') 26 | } 27 | } 28 | ``` -------------------------------------------------------------------------------- /doc/exemples/PresenceProvider.md: -------------------------------------------------------------------------------- 1 | # 📌 Set application presence from provider 2 | 3 | A concrete example would be the console display of the files instantiated in your application : 4 | 5 | ```ts 6 | import { Application } from 'ioc:factory/Core' 7 | import { BaseProvider, EntityResolvable } from 'ioc:factory/Core/Provider' 8 | import Logger from '@leadcodedev/logger' 9 | 10 | export default class AppProvider implements Provider { 11 | public async boot(): Promise { 12 | Logger.send('info', 'Application start') 13 | // Your code here 14 | } 15 | 16 | public async load (Class: EntityResolvable): Promise { 17 | if (Class instanceof CommandEntity) { 18 | Logger.send('info', `The file named ${Class.ctx.name} was loaded`) 19 | } 20 | } 21 | 22 | // When your bot is ready, You can set its presence like this 23 | public async ok (): Promise { 24 | // Get client 25 | const client: Client = Application.getClient() 26 | 27 | // Set presence 28 | await client.user.setPresence({ 29 | status: 'idle', 30 | afk: false, 31 | activities: [ 32 | { name: 'Trying presence', type: 'STREAMING', url: 'streaming url' } 33 | ] 34 | }) 35 | 36 | const { presence, username } = client.user 37 | Logger.send('info', `Application is ready and the presence of ${username} is define to ${presence.status}`) 38 | } 39 | } 40 | ``` 41 | 42 | The context object collects the various file types in your application -------------------------------------------------------------------------------- /doc/partials/Command.md: -------------------------------------------------------------------------------- 1 | ```ts 2 | import { BaseCommand, Command } from 'ioc:factory/Core/Command' 3 | import { CommandInteraction } from 'discord.js' 4 | 5 | @Command({ 6 | scope: 'GUILDS', 7 | options: { 8 | name: 'foo', 9 | description: 'Your foo command description', 10 | options: [], 11 | }, 12 | }) 13 | export default class FooCommand implements BaseCommand { 14 | public async run(interaction: CommandInteraction): Promise { 15 | // Your code here 16 | } 17 | } 18 | 19 | ``` -------------------------------------------------------------------------------- /doc/partials/ContextMenu.md: -------------------------------------------------------------------------------- 1 | ```ts 2 | import { BaseContextMenu, ContextMenu } from 'ioc:factory/Core/ContextMenu' 3 | import { ContextMenuInteraction } from 'discord.js' 4 | 5 | @ContextMenu({ 6 | scope: 'GUILDS', 7 | options: { 8 | name: 'foo', 9 | type: 'USER' 10 | }, 11 | }) 12 | export default class FooContextMenu implements BaseContextMenu { 13 | public async run(interaction: ContextMenuInteraction): Promise { 14 | // Your code here 15 | } 16 | } 17 | 18 | ``` -------------------------------------------------------------------------------- /doc/partials/Event.md: -------------------------------------------------------------------------------- 1 | ```ts 2 | import { Event, BaseEvent } from 'ioc:factory/Core/Event' 3 | import { GuildMember, VoiceChannel } from 'discord.js' 4 | 5 | @Event('voiceMemberJoin') 6 | export default class FooEvent implements BaseEvent { 7 | public async run(member: GuildMember, channel: VoiceChannel): Promise { 8 | // Your code here 9 | } 10 | } 11 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@discord-factory/core-next", 3 | "version": "4.0.1", 4 | "description": "", 5 | "author": "Baptiste Parmantier ", 6 | "license": "MIT", 7 | "repository": "git@github.com:DiscordFactory/Core.git", 8 | "keywords": [], 9 | "main": "./build/index.js", 10 | "scripts": { 11 | "dev": "tsc --watch", 12 | "build": "tsc", 13 | "lint": "eslint \"{src,test}/**/*.ts\"", 14 | "lint:fix": "npm run lint -- --fix", 15 | "test": "ava" 16 | }, 17 | "devDependencies": { 18 | "@babel/core": "^7.15.5", 19 | "@types/node": "^14.14.37", 20 | "ava": "^3.15.0", 21 | "eslint": "^7.23.0", 22 | "eslint-plugin-ava": "^12.0.0", 23 | "typescript": "^4.2.3" 24 | }, 25 | "dependencies": { 26 | "@babel/eslint-parser": "^7.15.4", 27 | "@leadcodedev/logger": "^1.0.0", 28 | "@typescript-eslint/eslint-plugin": "^4.31.1", 29 | "@typescript-eslint/parser": "^4.31.1", 30 | "cac": "^6.7.8", 31 | "cli-table2": "^0.2.0", 32 | "cross-env": "^7.0.3", 33 | "discord.js": "^13.6.0", 34 | "fs-recursive": "^1.1.7", 35 | "js-yaml": "^4.1.0", 36 | "module-alias": "^2.2.2" 37 | }, 38 | "ava": { 39 | "extensions": [ 40 | "ts" 41 | ], 42 | "require": [ 43 | "ts-node/register" 44 | ], 45 | "files": [ 46 | "test/**/*.spec.ts" 47 | ], 48 | "verbose": true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Application.ts: -------------------------------------------------------------------------------- 1 | import Factory from './Factory' 2 | import { Client, ClientEvents, Collection } from 'discord.js' 3 | import { CommandEntity } from './entities/Command' 4 | import { EventEntity } from './entities/Event' 5 | import { ContextMenuEntity } from './entities/ContextMenu' 6 | 7 | export default class Application { 8 | public static getClient (): Client { 9 | return Factory.getInstance().client! 10 | } 11 | 12 | public static getCommands (): CommandEntity[] { 13 | return Factory.getInstance().ignitor.getContainer().commands 14 | } 15 | 16 | public static getContextMenu (): ContextMenuEntity[] { 17 | return Factory.getInstance().ignitor.getContainer().contextMenu 18 | } 19 | 20 | public static getEvents (): EventEntity[] { 21 | return Factory.getInstance().ignitor.getContainer().events 22 | } 23 | 24 | public static getCliCommands (): Collection { 25 | return Factory.getInstance().ignitor.getContainer().cli 26 | } 27 | 28 | public static getEnvironment (): { [p: string]: unknown } { 29 | return Factory.getInstance().ignitor.environmentBuilder.environment!.content 30 | } 31 | 32 | public static getEnvironmentValue (key: string): unknown | undefined { 33 | return Factory.getInstance().ignitor.getEnvironment(key) 34 | } 35 | } -------------------------------------------------------------------------------- /src/Container.ts: -------------------------------------------------------------------------------- 1 | import { ClientEvents, Collection } from 'discord.js' 2 | import { EventEntity } from './entities/Event' 3 | import { CommandEntity } from './entities/Command' 4 | import { ProviderEntity } from './entities/Provider' 5 | import { ContextMenuEntity } from './entities/ContextMenu' 6 | 7 | export default class Container { 8 | public events: EventEntity[] = [] 9 | public commands: CommandEntity[] = [] 10 | public contextMenu: ContextMenuEntity[] = [] 11 | public providers: ProviderEntity[] = [] 12 | public cli: Collection = new Collection() 13 | } -------------------------------------------------------------------------------- /src/Environment.ts: -------------------------------------------------------------------------------- 1 | import { EnvironmentType } from './types' 2 | 3 | export default class Environment { 4 | constructor ( 5 | public type: EnvironmentType, 6 | public content: { [K in string]: any } 7 | ) { 8 | } 9 | } -------------------------------------------------------------------------------- /src/EnvironmentBuilder.ts: -------------------------------------------------------------------------------- 1 | import { fetchSortedExpression } from 'fs-recursive' 2 | import Environment from './Environment' 3 | import { EnvironmentType } from './types' 4 | import YAML from 'js-yaml' 5 | 6 | export default class EnvironmentBuilder { 7 | public environment: Environment | undefined 8 | 9 | public async fetch () { 10 | const environments = await fetchSortedExpression( 11 | process.cwd(), 12 | process.env.NODE_ENV === 'production' 13 | ? /^environment\.prod\.(json|yml|yaml)/ 14 | : /^environment\.dev\.(json|yml|yaml)/, 15 | ['json', 'yml', 'yaml'], 16 | 'utf-8', 17 | ['node_modules'] 18 | ) 19 | 20 | if (!environments.length) { 21 | throw new Error(`${process.env.NODE_ENV === 'production' 22 | ? 'environment.prod.(json|yml|yaml)' 23 | : 'environment.dev.(json|yml|yaml)' 24 | } file is missing, please create one in the root project.`) 25 | } 26 | 27 | const environment = environments[0] 28 | let environmentContent = {} 29 | 30 | if (environment.extension === 'json') { 31 | const file = await environment.getContent('utf-8') 32 | environmentContent = JSON.parse(file!.toString()) 33 | } 34 | 35 | if (environment.extension === 'yaml' || environment.extension === 'yml') { 36 | const file = await environment.getContent('utf-8') 37 | environmentContent = YAML.load(file, 'utf8') 38 | } 39 | 40 | this.environment = new Environment( 41 | environment.extension as EnvironmentType, 42 | environmentContent 43 | ) 44 | } 45 | } -------------------------------------------------------------------------------- /src/Factory.ts: -------------------------------------------------------------------------------- 1 | import EventManager from './managers/EventManager' 2 | import { Client, Collection, Guild, OAuth2Guild, RateLimitData, ShardingManager, Snowflake } from 'discord.js' 3 | import Ignitor from './Ignitor' 4 | import HookManager from './managers/HookManager' 5 | import NodeEmitter from './utils/NodeEmitter' 6 | import ProviderManager from './managers/ProviderManager' 7 | import { ProviderEntity } from './entities/Provider' 8 | import { DiscordEventManager } from './managers/DiscordEventManager' 9 | import VoiceJoin from './events/VoiceJoin' 10 | import VoiceLeave from './events/VoiceLeave' 11 | import GuildMemberAddBoost from './events/GuildMemberAddBoost' 12 | import GuildMemberRemoveBoost from './events/GuildMemberRemoveBoost' 13 | import Logger from '@leadcodedev/logger' 14 | import BaseCommandManager from './managers/BaseCommandManager' 15 | 16 | export default class Factory { 17 | private static $instance: Factory 18 | 19 | public client: Client | undefined 20 | public shardManager: ShardingManager | undefined 21 | public guildIds: Snowflake[] = [] 22 | 23 | public readonly eventManager: EventManager = new EventManager(this) 24 | public readonly hookManager: HookManager = new HookManager(this) 25 | public readonly baseCommandManager: BaseCommandManager = new BaseCommandManager(this) 26 | public readonly providerManager: ProviderManager = new ProviderManager(this) 27 | public readonly discordEventManager: DiscordEventManager = new DiscordEventManager(this) 28 | 29 | constructor (public ignitor: Ignitor) { 30 | } 31 | 32 | public async createClient () { 33 | const SHARDS = this.ignitor.environmentBuilder.environment!.content.SHARDS 34 | 35 | this.client = new Client({ 36 | intents: this.ignitor.environmentBuilder.environment?.content.INTENTS, 37 | partials: this.ignitor.environmentBuilder.environment?.content.PARTIALS, 38 | ...SHARDS && SHARDS.MODE === 'AUTO' && { shards: 'auto' }, 39 | }) 40 | 41 | await this.client?.login(this.ignitor.environmentBuilder.environment?.content.APP_TOKEN) 42 | NodeEmitter.emit('application::client::login', this.client) 43 | } 44 | 45 | public static getInstance (ignitor?: Ignitor) { 46 | if (!this.$instance && ignitor) { 47 | this.$instance = new Factory(ignitor) 48 | } 49 | return this.$instance 50 | } 51 | 52 | public async init () { 53 | const SHARDS = this.ignitor.environmentBuilder.environment!.content.SHARDS 54 | 55 | if (!SHARDS || SHARDS.MODE !== 'FILE') { 56 | await this.createClient() 57 | } 58 | 59 | await this.providerManager.register() 60 | this.ignitor.container.providers.forEach((provider: ProviderEntity) => provider.boot()) 61 | const guildCollection = await this.client?.guilds.fetch() as Collection 62 | this.guildIds = guildCollection.map((guild: OAuth2Guild) => guild.id) 63 | 64 | this.client?.on('rateLimit', (rateLimit: RateLimitData) => { 65 | Logger.send('info', `The application has been rate limited, please try again in ${rateLimit.timeout / 1000} seconds`) 66 | Logger.send('info', `method: ${rateLimit.method} | path: ${rateLimit.path}`) 67 | }) 68 | 69 | this.client?.on('guildCreate', async (guild: Guild) => { 70 | this.guildIds.push(guild.id) 71 | await this.baseCommandManager.add(guild) 72 | }) 73 | 74 | this.client?.on('guildDelete', async (guild: Guild) => { 75 | const index = this.guildIds.findIndex((id: Snowflake) => id === guild.id) 76 | this.guildIds.splice(index, 1) 77 | }) 78 | 79 | await this.ignitor.addonManager.registerAddons() 80 | await this.hookManager.register() 81 | 82 | await Promise.all([ 83 | this.eventManager.register(), 84 | this.baseCommandManager.setup(), 85 | this.discordEventManager.register( 86 | new VoiceLeave(this), 87 | new VoiceJoin(this), 88 | new GuildMemberAddBoost(this), 89 | new GuildMemberRemoveBoost(this), 90 | ), 91 | ]) 92 | 93 | this.ignitor.container.providers.forEach((provider: ProviderEntity) => provider.ok()) 94 | NodeEmitter.emit('application::ok', this.client) 95 | return this 96 | } 97 | } -------------------------------------------------------------------------------- /src/Ignitor.ts: -------------------------------------------------------------------------------- 1 | import Factory from './Factory' 2 | import { fetch } from 'fs-recursive' 3 | import { Collection } from 'discord.js' 4 | import AddonManager from './managers/AddonManager' 5 | import Container from './Container' 6 | import path from 'path' 7 | import ModuleAlias from 'module-alias' 8 | import NodeEmitter from './utils/NodeEmitter' 9 | import { EnvironmentType } from './types' 10 | import EnvironmentBuilder from './EnvironmentBuilder' 11 | import CliManager from './managers/CliManager' 12 | import Help from './cli/help' 13 | import Version from './cli/version' 14 | import { Command } from 'cac' 15 | 16 | export default class Ignitor { 17 | public files: Collection = new Collection() 18 | public factory: Factory | undefined 19 | public kernel: any | undefined 20 | public environmentBuilder: EnvironmentBuilder = new EnvironmentBuilder() 21 | 22 | public readonly container: Container = new Container() 23 | public readonly addonManager: AddonManager = new AddonManager(this) 24 | public readonly cliManager: CliManager = new CliManager(this) 25 | 26 | public async createFactory () { 27 | this.registerAlias() 28 | 29 | await this.getEnvironnement() 30 | await this.loadFiles('src') 31 | await this.loadFiles('providers') 32 | 33 | await this.loadKernel() 34 | 35 | this.factory = Factory.getInstance(this) 36 | await this.factory.init() 37 | 38 | NodeEmitter.emit('application::starting') 39 | 40 | return this 41 | } 42 | 43 | public async createCommand () { 44 | this.registerAlias() 45 | 46 | await this.getEnvironnement() 47 | await this.loadKernel() 48 | await this.addonManager.registerAddons() 49 | this.cliManager.register( 50 | new (Version as any)(), 51 | new (Help as any)(), 52 | ) 53 | return this.cliManager.cli 54 | } 55 | 56 | private async getEnvironnement () { 57 | await this.environmentBuilder.fetch() 58 | } 59 | 60 | private async loadFiles (dir) { 61 | const baseDir = process.env.NODE_ENV === 'production' 62 | ? path.join(process.cwd(), 'build', dir) 63 | : path.join(process.cwd(), dir) 64 | 65 | const fetchedFiles = await fetch( 66 | baseDir, 67 | [process.env.NODE_ENV === 'production' ? 'js' : 'ts'], 68 | 'utf-8', 69 | ['node_modules', 'test'] 70 | ) 71 | 72 | const files = Array.from(fetchedFiles, ([key, file]) => ({ key, ...file })) 73 | await Promise.all( 74 | files.map(async (file) => { 75 | const res = await import(file.path) 76 | 77 | if (res?.default?.type) { 78 | this.files.set(file.key, { 79 | type: res.default.type, 80 | default: res.default, 81 | file, 82 | }) 83 | } 84 | })) 85 | } 86 | 87 | private async loadKernel () { 88 | const kernelPath = process.env.NODE_ENV === 'production' 89 | ? path.join(process.cwd(), 'build', 'start', 'Kernel.js') 90 | : path.join(process.cwd(), 'start', 'Kernel.ts') 91 | 92 | const item = await import(kernelPath) 93 | this.kernel = new item.default() 94 | } 95 | 96 | private registerAlias () { 97 | ModuleAlias.addAliases({ 98 | App: process.env.NODE_ENV === 'production' 99 | ? path.join(process.cwd(), 'build', 'src') 100 | : path.join(process.cwd(), 'src'), 101 | }) 102 | ModuleAlias.addAlias('ioc:factory/Core', () => path.join(process.cwd(), 'node_modules', '@discord-factory', 'core-next')) 103 | ModuleAlias.addAlias('ioc:factory/Core/Provider', () => path.join(process.cwd(), 'node_modules', '@discord-factory', 'core-next')) 104 | ModuleAlias.addAlias('ioc:factory/Core/Event', () => path.join(process.cwd(), 'node_modules', '@discord-factory', 'core-next')) 105 | ModuleAlias.addAlias('ioc:factory/Core/Command', () => path.join(process.cwd(), 'node_modules', '@discord-factory', 'core-next')) 106 | ModuleAlias.addAlias('ioc:factory/Core/ContextMenu', () => path.join(process.cwd(), 'node_modules', '@discord-factory', 'core-next')) 107 | ModuleAlias.addAlias('ioc:factory/Core/Hook', () => path.join(process.cwd(), 'node_modules', '@discord-factory', 'core-next')) 108 | ModuleAlias.addAlias('ioc:factory/Core/Container', () => path.join(process.cwd(), 'node_modules', '@discord-factory', 'core-next')) 109 | } 110 | 111 | 112 | 113 | public getModuleEnvironment (module: string, key: string) { 114 | const element = this.getEnvironment(module.toUpperCase()) 115 | return element[key] 116 | } 117 | 118 | public getEnvironment (key: string): any | undefined { 119 | const pathChain = key.split('.') 120 | if (pathChain.length > 1) { 121 | let result = this.environmentBuilder.environment?.content 122 | pathChain.forEach(element => result = result?.[element]) 123 | return result 124 | } 125 | else return this.environmentBuilder.environment?.content[key] 126 | } 127 | 128 | public getContainer (): Container { 129 | return this.container 130 | } 131 | 132 | public getFiles (): Collection { 133 | return this.files 134 | } 135 | 136 | public getSelectEnvironment (): EnvironmentType { 137 | return this.environmentBuilder.environment!.type 138 | } 139 | 140 | public async exec () { 141 | const cli = await this.createCommand() 142 | const commands: Collection = new Collection() 143 | 144 | cli.commands.forEach((command) => { 145 | command.aliasNames.forEach((alias: string) => { 146 | commands.set(alias, command) 147 | }) 148 | }) 149 | 150 | if (!process.argv[2]) { 151 | process.argv[2] = 'help' 152 | } 153 | 154 | const command = cli.commands.find((command: Command) => command.aliasNames.includes(process.argv[2])) 155 | if (command) { 156 | process.argv[2] = command.name 157 | } 158 | 159 | cli.parse() 160 | } 161 | } -------------------------------------------------------------------------------- /src/cli/help.ts: -------------------------------------------------------------------------------- 1 | import { BaseCli, CLI } from '../entities/Cli' 2 | import { Command } from 'cac' 3 | import { Color } from '../utils/ConsoleColors' 4 | import { isUsingYarn, table } from '../utils' 5 | import { CliContextRuntime } from '../types' 6 | 7 | @CLI({ 8 | prefix: 'help', 9 | description: 'Help command cli', 10 | alias: ['--help', '-h'], 11 | config: { 12 | allowUnknownOptions: false, 13 | ignoreOptionDefaultValue: true 14 | } 15 | }) 16 | export default class Help extends BaseCli { 17 | public async run ({ cli }: CliContextRuntime): Promise { 18 | const registeredCommands = cli.commands 19 | .map((command: Command) => { 20 | const body = Color.Reset + Color.Dim + command.description + Color.Reset 21 | const heading = [` • ${Color.Bright + Color.White + command.name}`] 22 | 23 | if (command.aliasNames.length) { 24 | heading.push(`(${command.aliasNames.join(', ')})`) 25 | } 26 | 27 | return `${heading.join(' ')} ${body}` 28 | }) 29 | .sort() 30 | .join('\n') 31 | 32 | const usagePackageManager = isUsingYarn() 33 | ? `yarn ${cli.name}` 34 | : `npm run ${cli.name}` 35 | 36 | const options = Object.entries(cli.options).map(([key, value]) => ` • ${key} ${value.join(', ')}`) 37 | 38 | const instructions = 39 | `Usage :\n` + 40 | ` $ ${usagePackageManager} [options]\n\n` + 41 | `Commands :\n` + 42 | `${registeredCommands}\n` + ( 43 | options.length > 1 44 | ? (`Options :\n` + `${options.join('\n')}`) 45 | : '' 46 | ) 47 | 48 | table.push( 49 | ['Help menu'] as any, 50 | [instructions] as any, 51 | ) 52 | 53 | console.log('\n' + table.toString() + '\n') 54 | } 55 | } -------------------------------------------------------------------------------- /src/cli/version.ts: -------------------------------------------------------------------------------- 1 | import { BaseCli, CLI } from '../entities/Cli' 2 | import path from 'path' 3 | import { table } from '../utils' 4 | import { CliContextRuntime } from '../types' 5 | 6 | @CLI({ 7 | prefix: 'version', 8 | description: 'Displays the version of the Factory packages', 9 | alias: ['--version', '-v'], 10 | config: { 11 | allowUnknownOptions: false, 12 | ignoreOptionDefaultValue: true 13 | } 14 | }) 15 | export default class Version extends BaseCli { 16 | public async run ({ cli }: CliContextRuntime): Promise { 17 | const jsonPackage = await import(path.join(process.cwd(), 'package.json')) 18 | const discordFactoryPackages = Object.entries(jsonPackage.dependencies).map(([key, version]) => { 19 | const discriminator = '@discord-factory/' 20 | if (key.startsWith(discriminator)) { 21 | const packageName = key 22 | .replace(new RegExp(discriminator, ''), '') 23 | .replace(/-/g, ' ') 24 | return `• ${packageName.charAt(0).toUpperCase() + packageName.slice(1, packageName.length)} : ${version}` 25 | } 26 | }).filter((a) => a) 27 | 28 | const instructions = 29 | `${discordFactoryPackages.join('\n')}\n` 30 | 31 | table.push( 32 | ['Packages versions'] as any, 33 | [instructions] as any, 34 | ) 35 | 36 | console.log('\n' + table.toString() + '\n') 37 | } 38 | } -------------------------------------------------------------------------------- /src/entities/Addon.ts: -------------------------------------------------------------------------------- 1 | import { AddonContext } from '../types' 2 | 3 | export function CLICommand (options: { name: string, prefix: string, usages: string[] }) { 4 | return (target: Function) => { 5 | return class Command extends CliCommandEntity { 6 | constructor (context: any) { 7 | super( 8 | context, 9 | options.name, 10 | options.prefix, 11 | options.usages, 12 | target.prototype.run 13 | ) 14 | } 15 | } as any 16 | } 17 | } 18 | 19 | export abstract class BaseAddonCommand { 20 | public context: AddonContext | undefined 21 | public abstract run (...params: string[]): Promise 22 | } 23 | 24 | export interface AddonCommand { 25 | name: string 26 | prefix: string 27 | params: string 28 | run (): Promise 29 | } 30 | 31 | export class CliCommandEntity { 32 | constructor ( 33 | public context: AddonContext | undefined, 34 | public name: string, 35 | public prefix: string, 36 | public usages: string[], 37 | public run: (...args: Array) => Promise, 38 | ) { 39 | } 40 | } 41 | 42 | export abstract class BaseAddonHook { 43 | public context: AddonContext | undefined 44 | public abstract run (...props: any[]): Promise 45 | } 46 | 47 | export abstract class BaseAddonEvent { 48 | public context: AddonContext | undefined 49 | public abstract run (...props: any[]): Promise 50 | } 51 | 52 | export abstract class BaseAddon { 53 | public abstract addonName: string 54 | public abstract registerCLI (): any[] 55 | public abstract registerEvents (): any[] 56 | public abstract registerCommands (): any[] 57 | public abstract registerHooks (): any[] 58 | public abstract defineKeys (): string[] 59 | 60 | protected constructor (public context: AddonContext) { 61 | } 62 | } -------------------------------------------------------------------------------- /src/entities/Cli.ts: -------------------------------------------------------------------------------- 1 | import { AddonContext, CliCommandContext, CliContextRuntime } from '../types' 2 | 3 | export function CLI (context: CliCommandContext) { 4 | return (target: Function) => { 5 | target.prototype.prefix = context.prefix 6 | target.prototype.description = context.description 7 | target.prototype.args = context.args 8 | target.prototype.config = context.config 9 | target.prototype.options = context.options 10 | target.prototype.alias = context.alias 11 | target.prototype.exemple = context.exemple 12 | } 13 | } 14 | 15 | export interface CliCommand extends CliCommandContext { 16 | run (context: CliContextRuntime): Promise 17 | } 18 | 19 | export abstract class BaseCli { 20 | public abstract run (context: CliContextRuntime): Promise 21 | protected constructor (public context: AddonContext | undefined) { 22 | } 23 | } -------------------------------------------------------------------------------- /src/entities/Command.ts: -------------------------------------------------------------------------------- 1 | import { AddonContext, CommandContext, ContainerType, CommandGlobalContext, ScopeContext } from '../types' 2 | import Constructable from '../utils/Constructable' 3 | import { ApplicationCommandPermissionData, CommandInteraction } from 'discord.js' 4 | import EntityFile from '../utils/EntityFile' 5 | import Cooldown from '../utils/Cooldown' 6 | 7 | export function Command (ctx: CommandGlobalContext) { 8 | return (target: Function) => { 9 | return class SlashCommand extends CommandEntity { 10 | constructor (context: any) { 11 | super( 12 | context, 13 | ctx.scope, 14 | ctx.cooldown?.count || ctx.cooldown?.time 15 | ? new Cooldown(ctx.cooldown) 16 | : undefined, 17 | { ...ctx.options, name: ctx.options.name.toLowerCase() }, 18 | target.prototype.run, 19 | ) 20 | } 21 | } as any 22 | } 23 | } 24 | 25 | export class CommandEntity extends Constructable { 26 | public static type: ContainerType = 'command' 27 | 28 | constructor ( 29 | public context: AddonContext | undefined, 30 | public scope: ScopeContext, 31 | public cooldown: Cooldown | undefined, 32 | public ctx: CommandContext, 33 | public run: (...args: any[]) => Promise, 34 | public file?: EntityFile | undefined, 35 | ) { 36 | super(file) 37 | } 38 | } 39 | 40 | export abstract class BaseCommand { 41 | public abstract run (interaction: CommandInteraction): Promise 42 | } -------------------------------------------------------------------------------- /src/entities/ContextMenu.ts: -------------------------------------------------------------------------------- 1 | import { AddonContext, ApplicationContextOption, ContainerType, ApplicationGlobalContext, ScopeContext } from '../types' 2 | import Constructable from '../utils/Constructable' 3 | import { ApplicationCommandPermissionData, ContextMenuInteraction } from 'discord.js' 4 | import EntityFile from '../utils/EntityFile' 5 | import Cooldown from '../utils/Cooldown' 6 | 7 | export function ContextMenu (ctx: ApplicationGlobalContext) { 8 | return (target: Function) => { 9 | return class ContextMenu extends ContextMenuEntity { 10 | constructor (context: any) { 11 | super( 12 | context, 13 | ctx.scope, 14 | ctx.cooldown?.count || ctx.cooldown?.time 15 | ? new Cooldown(ctx.cooldown) 16 | : undefined, 17 | { ...ctx.options, type: ctx.options.type as any }, 18 | target.prototype.run, 19 | ) 20 | } 21 | } as any 22 | } 23 | } 24 | 25 | export class ContextMenuEntity extends Constructable { 26 | public static type: ContainerType = 'context-menu' 27 | 28 | constructor ( 29 | public context: AddonContext | undefined, 30 | public scope: ScopeContext, 31 | public cooldown: Cooldown | undefined, 32 | public ctx: ApplicationContextOption, 33 | public run: (...args: any[]) => Promise, 34 | public file?: EntityFile | undefined, 35 | ) { 36 | super(file) 37 | } 38 | } 39 | 40 | export abstract class BaseContextMenu { 41 | public abstract run (interaction: ContextMenuInteraction): Promise 42 | } -------------------------------------------------------------------------------- /src/entities/Event.ts: -------------------------------------------------------------------------------- 1 | import Constructable from '../utils/Constructable' 2 | import { AddonContext, Events } from '../types' 3 | import EntityFile from '../utils/EntityFile' 4 | 5 | export function Event (identifier: K) { 6 | return (target: Function) => { 7 | return class Event extends EventEntity { 8 | constructor (context: any) { 9 | super(context, identifier, target.prototype.run, undefined) 10 | } 11 | } as any 12 | } 13 | } 14 | 15 | export class EventEntity extends Constructable { 16 | public static type: string = 'event' 17 | 18 | constructor ( 19 | public context: AddonContext | undefined, 20 | public event: K, 21 | public run: (...args: Array) => Promise, 22 | public file: EntityFile | undefined 23 | ) { 24 | super(file) 25 | } 26 | } 27 | 28 | export abstract class BaseEvent { 29 | public abstract run (...args: any[]): Promise 30 | } -------------------------------------------------------------------------------- /src/entities/Hook.ts: -------------------------------------------------------------------------------- 1 | import Constructable from '../utils/Constructable' 2 | import { HookType } from '../types' 3 | import { BaseAddon } from './Addon' 4 | import EntityFile from '../utils/EntityFile' 5 | 6 | export function Hook (identifier: string) { 7 | return (target: Function) => { 8 | return class Hook extends HookEntity { 9 | constructor (context: any) { 10 | super(context, identifier as HookType, target.prototype.run, undefined) 11 | } 12 | } as any 13 | } 14 | } 15 | 16 | export class HookEntity extends Constructable { 17 | public static type: string = 'hook' 18 | 19 | constructor ( 20 | public context: BaseAddon | undefined, 21 | public type: HookType, 22 | public run: (...args: any[]) => Promise, 23 | public file: EntityFile | undefined, 24 | ) { 25 | super(file) 26 | } 27 | } 28 | 29 | export abstract class BaseHook { 30 | public abstract run (...args: any[]): Promise 31 | } -------------------------------------------------------------------------------- /src/entities/Provider.ts: -------------------------------------------------------------------------------- 1 | import { File } from 'fs-recursive' 2 | import Constructable from '../utils/Constructable' 3 | import { EntityResolvable } from '../types' 4 | 5 | export class ProviderEntity extends Constructable { 6 | public static type: string = 'provider' 7 | 8 | constructor ( 9 | public boot: () => Promise, 10 | public load: (file: EntityResolvable) => Promise, 11 | public ok: () => Promise, 12 | public file: File | undefined, 13 | ) { 14 | super(file) 15 | } 16 | } 17 | 18 | export abstract class BaseProvider { 19 | public abstract boot: () => Promise 20 | public abstract load: (file: EntityResolvable) => Promise 21 | public abstract ok: () => Promise 22 | } -------------------------------------------------------------------------------- /src/events/GuildMemberAddBoost.ts: -------------------------------------------------------------------------------- 1 | import Factory from '../Factory' 2 | 3 | export default class GuildMemberAddBoost { 4 | constructor (public factory: Factory) { 5 | } 6 | 7 | public async handle () { 8 | this.factory.client?.ws.on('GUILD_MEMBER_UPDATE', (payload) => { 9 | const guild = this.factory.client?.guilds.cache.get(payload.guild_id) 10 | const member = guild?.members.cache.get(payload.user.id) 11 | 12 | if (!payload.premium_since) { 13 | return 14 | } 15 | 16 | if (member!.premiumSince! < new Date(payload.premium_since)) { 17 | this.factory.client?.emit('guildMemberAddBoost', member) 18 | } 19 | }) 20 | } 21 | } -------------------------------------------------------------------------------- /src/events/GuildMemberRemoveBoost.ts: -------------------------------------------------------------------------------- 1 | import Factory from '../Factory' 2 | 3 | export default class GuildMemberRemoveBoost { 4 | constructor (public factory: Factory) { 5 | } 6 | 7 | public async handle () { 8 | this.factory.client?.ws.on('GUILD_MEMBER_UPDATE', (payload) => { 9 | const guild = this.factory.client?.guilds.cache.get(payload.guild_id) 10 | const member = guild?.members.cache.get(payload.user.id) 11 | 12 | if (!payload.premium_since) { 13 | return 14 | } 15 | 16 | if (member!.premiumSince! && payload.premium_since) { 17 | this.factory.client?.emit('guildMemberRemoveBoost', member) 18 | } 19 | }) 20 | } 21 | } -------------------------------------------------------------------------------- /src/events/VoiceJoin.ts: -------------------------------------------------------------------------------- 1 | import Factory from '../Factory' 2 | 3 | export default class VoiceJoin { 4 | constructor (public factory: Factory) { 5 | } 6 | public async handle () { 7 | this.factory.client?.on('raw', ({ t: event, d: payload }) => { 8 | if (event !== 'VOICE_STATE_UPDATE' || !payload.channel_id) { 9 | return 10 | } 11 | 12 | const guild = this.factory!.client!.guilds.resolve(payload.guild_id) 13 | const member = guild?.members.resolve(payload.user_id) 14 | 15 | this.factory?.client?.emit('voiceJoin', { 16 | ...member.voice, 17 | channelId: payload.channel_id, 18 | channel: guild.channels.resolve(payload.channel_id), 19 | member, 20 | }) 21 | }) 22 | } 23 | } -------------------------------------------------------------------------------- /src/events/VoiceLeave.ts: -------------------------------------------------------------------------------- 1 | import Factory from '../Factory' 2 | 3 | export default class VoiceLeave { 4 | constructor (public factory: Factory) { 5 | } 6 | 7 | public async handle () { 8 | this.factory.client?.on('raw', ({ t: event, d: payload }) => { 9 | if (event !== 'VOICE_STATE_UPDATE') { 10 | return 11 | } 12 | 13 | const guild = this.factory.client?.guilds.resolve(payload.guild_id) 14 | if (!guild) { 15 | throw Error('Unable to resolve guild.') 16 | } 17 | const oldMember = guild.members.cache.get(payload.member.user.id) 18 | 19 | if (!oldMember?.voice.channel || oldMember?.voice.channel?.id === payload.channel_id) { 20 | return 21 | } 22 | 23 | const member = guild.members.resolve(payload.member.user.id) 24 | 25 | this.factory?.client?.emit('voiceLeave', { 26 | ...member.voice, 27 | channelId: payload.channel_id, 28 | channel: oldMember?.voice.channel, 29 | member, 30 | }) 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/events/WebsocketDebug.ts: -------------------------------------------------------------------------------- 1 | import Factory from '../Factory' 2 | 3 | export default class WebsocketDebug { 4 | constructor (public factory: Factory) { 5 | } 6 | 7 | public async handle (): Promise { 8 | this.factory.client?.on('raw', (payload: any) => { 9 | this.factory.client?.emit('websocketDebug', payload) 10 | }) 11 | } 12 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Ignitor from './Ignitor' 2 | import Factory from './Factory' 3 | import NodeEmitter from './utils/NodeEmitter' 4 | import { emptyReply } from './utils' 5 | import { AddonCommand, BaseAddonCommand, BaseAddon, BaseAddonHook, BaseAddonEvent } from './entities/Addon' 6 | import { CLI, BaseCli } from './entities/Cli' 7 | import { Event, BaseEvent, EventEntity } from './entities/Event' 8 | import { Command, BaseCommand, CommandEntity } from './entities/Command' 9 | import { ContextMenu, BaseContextMenu, ContextMenuEntity } from './entities/ContextMenu' 10 | import { Hook, BaseHook, HookEntity } from './entities/Hook' 11 | import { CommandContainer, EventContainer, HookContainer, ProviderContainer, EntityResolvable, AddonContext, CliContextRuntime } from './types' 12 | import { BaseProvider, ProviderEntity } from './entities/Provider' 13 | import Application from './Application' 14 | 15 | export { 16 | Ignitor, 17 | Factory, 18 | NodeEmitter, 19 | Application, 20 | 21 | AddonCommand, 22 | BaseAddonCommand, 23 | BaseAddon, 24 | CLI, 25 | BaseCli, 26 | CliContextRuntime, 27 | 28 | Event, 29 | BaseEvent, 30 | 31 | Command, 32 | BaseCommand, 33 | 34 | ContextMenu, 35 | BaseContextMenu, 36 | 37 | Hook, 38 | BaseHook, 39 | 40 | CommandContainer, 41 | EventContainer, 42 | HookContainer, 43 | ProviderContainer, 44 | 45 | BaseProvider, 46 | 47 | EntityResolvable, 48 | EventEntity, 49 | CommandEntity, 50 | ContextMenuEntity, 51 | HookEntity, 52 | ProviderEntity, 53 | 54 | BaseAddonHook, 55 | BaseAddonEvent, 56 | AddonContext, 57 | 58 | emptyReply 59 | } -------------------------------------------------------------------------------- /src/managers/AddonManager.ts: -------------------------------------------------------------------------------- 1 | import { BaseAddon } from '../entities/Addon' 2 | import Ignitor from '../Ignitor' 3 | import { EventEntity } from '../entities/Event' 4 | import { CommandEntity } from '../entities/Command' 5 | import { HookEntity } from '../entities/Hook' 6 | import NodeEmitter from '../utils/NodeEmitter' 7 | import { BaseCli } from '../entities/Cli' 8 | 9 | export default class AddonManager { 10 | constructor (public ignitor: Ignitor) { 11 | } 12 | 13 | public async registerAddons (): Promise { 14 | const addons: Function[] = await this.ignitor.kernel.registerAddons() 15 | await Promise.all( 16 | addons.map(async (item: any) => { 17 | const addonContext = { ...this.ignitor } 18 | 19 | const addon: BaseAddon = await new item(this.ignitor).init() 20 | addonContext['addon'] = addon 21 | 22 | const addonSectionName = addon.addonName.toUpperCase() 23 | 24 | const keys = addon.defineKeys() 25 | keys.forEach((key: string) => { 26 | if (!this.ignitor.getEnvironment(`${addonSectionName}.${key}`)) { 27 | throw new Error(`The "${key}" key is required in the "${addon.addonName.toUpperCase()}" module environment.`) 28 | } 29 | }) 30 | 31 | const cli = addon.registerCLI() 32 | const registeredCliCommands = cli.map((item: any) => { 33 | const command = new item(addonContext) 34 | this.registerCLI(command as BaseCli) 35 | return command 36 | }) 37 | 38 | 39 | const events = addon.registerEvents() 40 | const registeredEvents = events.map((item: any) => { 41 | const event = new item(addonContext) 42 | this.registerEvent(event as EventEntity) 43 | }) 44 | 45 | const commands = addon.registerCommands() 46 | commands.forEach((item: any) => { 47 | const command = new item(addonContext) 48 | this.registerCommand(command as CommandEntity) 49 | }) 50 | 51 | const hooks = addon.registerHooks() 52 | hooks.map(async (item: any) => { 53 | const hook = new item(addonContext) 54 | this.registerHooks(hook as HookEntity) 55 | }) 56 | 57 | return { 58 | events: registeredEvents, 59 | cliCommands: registeredCliCommands 60 | } 61 | }) 62 | ) 63 | } 64 | 65 | private registerCLI (Class: BaseCli) { 66 | // this.ignitor.container.cli.set(Class.prefix, Class) 67 | this.ignitor.cliManager.register(Class) 68 | } 69 | 70 | private registerEvent (Class: EventEntity) { 71 | this.ignitor.container.events.push(Class) 72 | this.ignitor.factory?.client?.on( 73 | Class.event, 74 | async (...args) => await Class.run(...args) 75 | ) 76 | } 77 | 78 | private registerCommand (Class: CommandEntity) { 79 | this.ignitor.container.commands.push(Class) 80 | } 81 | 82 | private registerHooks (Class: HookEntity) { 83 | NodeEmitter.on(Class.type, async (...props: any[]) => { 84 | await Class.run(...props) 85 | }) 86 | } 87 | } -------------------------------------------------------------------------------- /src/managers/BaseCommandManager.ts: -------------------------------------------------------------------------------- 1 | import SlashCommandManager from './commands/SlashCommandManager' 2 | import ContextMenuCommandManager from './commands/ContextMenuCommandManager' 3 | import Factory from '../Factory' 4 | import { Guild } from 'discord.js' 5 | import { CommandEntity } from '../entities/Command' 6 | import Logger from '@leadcodedev/logger' 7 | import { ContextMenuEntity } from '../entities/ContextMenu' 8 | import { catchPromise } from '../utils' 9 | 10 | export default class BaseCommandManager { 11 | public readonly commandManager: SlashCommandManager = new SlashCommandManager(this) 12 | public readonly contextMenuManager: ContextMenuCommandManager = new ContextMenuCommandManager(this) 13 | 14 | constructor (public factory: Factory) { 15 | } 16 | 17 | public async setup () { 18 | await Promise.all([ 19 | this.commandManager.register(), 20 | this.contextMenuManager.register() 21 | ]) 22 | } 23 | 24 | public async add (guild: Guild) { 25 | try { 26 | await guild.commands.fetch() 27 | } catch (error: any) { 28 | if (error.httpStatus === 403) { 29 | Logger.send('warn', `The guild "${guild.name}" (${guild.id}) does not accept command applications (scope : applications.commands).`) 30 | return 31 | } 32 | catchPromise(error) 33 | } 34 | 35 | const container = this.factory.ignitor.container 36 | const commands = [ 37 | ...container.commands, 38 | ...container.contextMenu, 39 | ] 40 | 41 | const guildCommandsFilter = (command: CommandEntity | ContextMenuEntity) => command.scope === 'GUILDS' || (Array.isArray(command.scope) && command.scope.includes(guild.id)) 42 | commands 43 | .filter(guildCommandsFilter) 44 | .forEach((command: CommandEntity | ContextMenuEntity ) => { 45 | guild.commands 46 | .create(command.ctx) 47 | .catch(catchPromise) 48 | }) 49 | } 50 | } -------------------------------------------------------------------------------- /src/managers/CliManager.ts: -------------------------------------------------------------------------------- 1 | import Ignitor from '../Ignitor' 2 | import { BaseCli, CliCommand } from '../entities/Cli' 3 | import { CAC, cac } from 'cac' 4 | 5 | export default class CliManager { 6 | public readonly cli: CAC = cac('factory') 7 | constructor (private ignitor: Ignitor) { 8 | } 9 | 10 | public register (...commands: BaseCli[]) { 11 | const cliCommands = commands as unknown as CliCommand[] 12 | cliCommands.forEach((command: CliCommand) => { 13 | this.registerCommand(command, command.prefix) 14 | }) 15 | } 16 | 17 | private registerCommand (command: CliCommand, prefix: string) { 18 | const args = {} 19 | if (command.args?.length) { 20 | process.argv 21 | .slice(3, process.argv.length) 22 | .forEach((item: string, key: number) => args[command.args![key]] = item) 23 | } 24 | 25 | const cliCommand = this.cli 26 | .command(prefix, command.description, command.config) 27 | .action(async (options) => { 28 | await command.run({ 29 | cli: this.cli, 30 | ignitor: this.ignitor, 31 | options, 32 | args 33 | }) 34 | }) 35 | 36 | if (command.alias?.length) { 37 | command.alias.forEach((alias) => { 38 | cliCommand.alias(alias) 39 | }) 40 | } 41 | 42 | if (command.options?.length) { 43 | command.options.forEach((option) => { 44 | cliCommand.option(option.name, option.description, option.config) 45 | }) 46 | } 47 | 48 | if (command.config?.ignoreOptionDefaultValue) cliCommand.config.ignoreOptionDefaultValue = command.config?.ignoreOptionDefaultValue 49 | if (command.config?.allowUnknownOptions) cliCommand.config.allowUnknownOptions = command.config?.allowUnknownOptions 50 | } 51 | } -------------------------------------------------------------------------------- /src/managers/DiscordEventManager.ts: -------------------------------------------------------------------------------- 1 | import Factory from '../Factory' 2 | 3 | export class DiscordEventManager { 4 | constructor (public factory: Factory) { 5 | } 6 | 7 | public async register (...events: any[]) { 8 | events.map(async (event) => { 9 | await event.handle() 10 | }) 11 | } 12 | } -------------------------------------------------------------------------------- /src/managers/EventManager.ts: -------------------------------------------------------------------------------- 1 | import Factory from '../Factory' 2 | import { EventEntity } from '../entities/Event' 3 | import NodeEmitter from '../utils/NodeEmitter' 4 | import { ProviderEntity } from '../entities/Provider' 5 | import EntityFile from '../utils/EntityFile' 6 | import { ClientEvents } from 'discord.js' 7 | import { Events } from '../types' 8 | 9 | export default class EventManager { 10 | constructor (public factory: Factory) { 11 | } 12 | 13 | public async register (): Promise { 14 | const files = this.factory.ignitor.files.filter((file: any) => file.type === 'event') 15 | 16 | await Promise.all( 17 | files.map(async (item: any) => { 18 | const instance = new item.default() 19 | const entityFile = new EntityFile(item.file.path) 20 | 21 | const event = new EventEntity( 22 | undefined, 23 | instance.event, 24 | instance.run, 25 | entityFile 26 | ) 27 | 28 | this.emit(event) 29 | 30 | this.factory.ignitor.container.providers.forEach((provider: ProviderEntity) => { 31 | provider.load(event) 32 | }) 33 | }) 34 | ) 35 | 36 | NodeEmitter.emit( 37 | 'application::events::registered', 38 | this.factory.ignitor.container.commands 39 | ) 40 | } 41 | 42 | private emit (instance: EventEntity) { 43 | if (!this.factory.shardManager?.shards.size) { 44 | this.factory.client!.on( 45 | instance.event as keyof ClientEvents, 46 | async (...args: any[]) => { 47 | await instance.run(...args) 48 | } 49 | ) 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/managers/HookManager.ts: -------------------------------------------------------------------------------- 1 | import Factory from '../Factory' 2 | import { HookEntity } from '../entities/Hook' 3 | import NodeEmitter from '../utils/NodeEmitter' 4 | import { ProviderEntity } from '../entities/Provider' 5 | import EntityFile from '../utils/EntityFile' 6 | 7 | export default class HookManager { 8 | 9 | constructor (public factory: Factory) { 10 | } 11 | 12 | public async register (): Promise { 13 | const files = this.factory.ignitor.files.filter((file: any) => file.type === 'hook') 14 | 15 | await Promise.all( 16 | files.map(async (item: any) => { 17 | const instance = new item.default() 18 | const entityFile = new EntityFile(item.file.path) 19 | 20 | const hook = new HookEntity( 21 | undefined, 22 | instance.type, 23 | instance.run, 24 | entityFile, 25 | ) 26 | 27 | NodeEmitter.on(hook.type, async (...props: any[]) => { 28 | await hook.run(...props) 29 | }) 30 | 31 | this.factory.ignitor.container.providers.forEach((provider: ProviderEntity) => { 32 | provider.load(hook) 33 | }) 34 | }) 35 | ) 36 | } 37 | } -------------------------------------------------------------------------------- /src/managers/ProviderManager.ts: -------------------------------------------------------------------------------- 1 | import Factory from '../Factory' 2 | import path from 'path' 3 | import { fetch } from 'fs-recursive' 4 | import { ProviderEntity } from '../entities/Provider' 5 | import EntityFile from '../utils/EntityFile' 6 | 7 | export default class ProviderManager { 8 | constructor (public factory: Factory) { 9 | } 10 | 11 | public async register (): Promise { 12 | const baseDir = process.env.NODE_ENV === 'production' 13 | ? path.join(process.cwd(), 'build', 'providers') 14 | : path.join(process.cwd(), 'providers') 15 | 16 | const fetchedFiles = await fetch( 17 | baseDir, 18 | [process.env.NODE_ENV === 'production' ? 'js' : 'ts'], 19 | 'utf-8', 20 | ['node_modules'] 21 | ) 22 | 23 | const files = Array.from(fetchedFiles, ([key, file]) => ({ key, ...file })) 24 | 25 | await Promise.all( 26 | files.map(async (item: any) => { 27 | const Class = await import(item.path) 28 | const instance = new Class.default() 29 | const entityFile = new EntityFile(item.path) 30 | 31 | const provider = new ProviderEntity( 32 | instance.boot, 33 | instance.load, 34 | instance.ok, 35 | entityFile, 36 | ) 37 | 38 | this.factory.ignitor.container.providers.push(provider) 39 | }) 40 | ) 41 | } 42 | } -------------------------------------------------------------------------------- /src/managers/commands/ContextMenuCommandManager.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApplicationCommand, 3 | ApplicationCommandManager, 4 | ApplicationCommandPermissions, 5 | Collection, 6 | Guild, 7 | GuildApplicationCommandManager, 8 | Interaction, 9 | Snowflake 10 | } from 'discord.js' 11 | import NodeEmitter from '../../utils/NodeEmitter' 12 | import { ProviderEntity } from '../../entities/Provider' 13 | import EntityFile from '../../utils/EntityFile' 14 | import BaseCommandManager from '../BaseCommandManager' 15 | import { catchPromise, isEquivalent } from '../../utils' 16 | import { ContextMenuEntity } from '../../entities/ContextMenu' 17 | import Logger from '@leadcodedev/logger' 18 | 19 | export default class SlashCommandManager { 20 | constructor (public commandManager: BaseCommandManager) { 21 | } 22 | 23 | public serialize (command: ApplicationCommand) { 24 | return { 25 | name: command.name, 26 | type: command.type, 27 | } 28 | } 29 | 30 | public async register (): Promise { 31 | const files = this.commandManager.factory.ignitor.files.filter((file: { type: string }) => file.type === 'context-menu') 32 | 33 | await Promise.all( 34 | files.map(async (item) => { 35 | const instance = new item.default() 36 | const entityFile = new EntityFile(item.file.path) 37 | 38 | const command = new ContextMenuEntity( 39 | undefined, 40 | instance.scope, 41 | instance.cooldown, 42 | instance.ctx, 43 | instance.run, 44 | entityFile 45 | ) 46 | 47 | this.commandManager.factory.ignitor.container.contextMenu.push(command) 48 | 49 | this.commandManager.factory.ignitor.container.providers.forEach((provider: ProviderEntity) => { 50 | provider.load(command) 51 | }) 52 | }) 53 | ) 54 | 55 | await this.preTreatment() 56 | } 57 | 58 | /** 59 | * Delete|Edit|Create 60 | * @param manager 61 | * @param commands 62 | * @param commandEntities 63 | * @protected 64 | */ 65 | protected treatment (manager: ApplicationCommandManager | GuildApplicationCommandManager, commands: Collection, commandEntities: ContextMenuEntity[]): void { 66 | if (commands.size === 0 && commandEntities.length === 0) { 67 | return 68 | } 69 | 70 | /** 71 | * Delete/Edit 72 | */ 73 | commands.forEach((command: ApplicationCommand) => { 74 | const filter = (commandEntity: ContextMenuEntity) => command.name === commandEntity.ctx.name 75 | const commandEntity = commandEntities.find(filter) 76 | 77 | if (!commandEntity) { 78 | command.delete().catch(catchPromise) 79 | return 80 | } 81 | 82 | const commandEntityIndex = commandEntities.findIndex(filter) 83 | if (commandEntityIndex !== undefined) { 84 | commandEntities.splice(commandEntityIndex, 1) 85 | } 86 | 87 | if (!isEquivalent(this.serialize(commandEntity.ctx as any), this.serialize(command))) { 88 | manager 89 | .edit(command.id, { 90 | ...commandEntity.ctx, 91 | }) 92 | .catch(catchPromise) 93 | } 94 | 95 | const definePermission = () => { 96 | const permissions = { 97 | command: command.id, 98 | } 99 | manager 100 | .edit(command.id, commandEntity.ctx) 101 | .catch(catchPromise) 102 | } 103 | }) 104 | 105 | /** 106 | * Create 107 | */ 108 | commandEntities.forEach((commandEntity: ContextMenuEntity) => { 109 | if (manager instanceof GuildApplicationCommandManager) { 110 | if (commandEntity.scope === 'GUILDS' || commandEntity.scope.includes(manager.guild.id)) { 111 | manager 112 | .create(commandEntity.ctx) 113 | .catch(catchPromise) 114 | } 115 | } else { 116 | manager 117 | .create(commandEntity.ctx) 118 | .catch(catchPromise) 119 | } 120 | }) 121 | 122 | NodeEmitter.emit( 123 | 'application::commands::registered', 124 | this.commandManager.factory.ignitor.container.commands 125 | ) 126 | } 127 | 128 | private preTreatment (): void { 129 | const client = this.commandManager.factory.client 130 | const commandContainer = this.commandManager.factory.ignitor.container.contextMenu 131 | 132 | const globalCommandContainer = commandContainer.filter((command: ContextMenuEntity) => command.scope === 'GLOBAL') 133 | const guildCommandContainer = commandContainer.filter((command: ContextMenuEntity) => ( 134 | command.scope === 'GUILDS' || Array.isArray(command.scope) 135 | )) 136 | 137 | client?.application?.commands 138 | .fetch() 139 | .then((commands: Collection) => { 140 | this.treatment(client!.application!.commands, commands.filter((command: ApplicationCommand) => command.type !== 'CHAT_INPUT'), globalCommandContainer) 141 | }) 142 | .catch(catchPromise) 143 | 144 | 145 | client?.guilds.cache.forEach((guild: Guild) => { 146 | guild.commands 147 | .fetch() 148 | .then((commands: Collection) => { 149 | this.treatment(guild.commands, commands.filter((command: ApplicationCommand) => command.type !== 'CHAT_INPUT'), guildCommandContainer) 150 | }) 151 | .catch((error) => { 152 | if (error.httpStatus === 403) { 153 | Logger.send('warn', `The guild "${guild.name}" (${guild.id}) does not accept command applications (scope : applications.commands).`) 154 | return 155 | } 156 | catchPromise(error) 157 | }) 158 | }) 159 | 160 | this.commandManager.factory.client?.on('interactionCreate', async (interaction: any) => { 161 | if (!interaction.isContextMenu()) { 162 | return 163 | } 164 | 165 | const command = commandContainer.find((command: ContextMenuEntity) => command.ctx.name.toLowerCase() === interaction.commandName.toLowerCase()) 166 | if (command) { 167 | command.cooldown?.setInteraction(interaction) 168 | 169 | const canExecute = command.cooldown 170 | ? await command.cooldown?.verify() 171 | : true 172 | 173 | if (canExecute) { 174 | await command.run(interaction) 175 | } 176 | } 177 | }) 178 | } 179 | } -------------------------------------------------------------------------------- /src/managers/commands/SlashCommandManager.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApplicationCommand, 3 | ApplicationCommandManager, 4 | ApplicationCommandPermissions, 5 | Collection, 6 | Guild, 7 | GuildApplicationCommandManager, 8 | Interaction, 9 | Snowflake 10 | } from 'discord.js' 11 | import { CommandEntity } from '../../entities/Command' 12 | import NodeEmitter from '../../utils/NodeEmitter' 13 | import { ProviderEntity } from '../../entities/Provider' 14 | import EntityFile from '../../utils/EntityFile' 15 | import BaseCommandManager from '../BaseCommandManager' 16 | import { catchPromise, isEquivalent } from '../../utils' 17 | import Logger from '@leadcodedev/logger' 18 | 19 | export default class SlashCommandManager { 20 | 21 | constructor (public commandManager: BaseCommandManager) { 22 | } 23 | 24 | public serialize (command: ApplicationCommand) { 25 | return { 26 | name: command.name, 27 | description: command.description, 28 | options: command.options.map(value => this.serializeOptions(value)), 29 | } 30 | } 31 | 32 | public serializeOptions (options: any) { 33 | return { 34 | name: options.name, 35 | description: options.description, 36 | type: options.type, 37 | required: options.required !== undefined 38 | ? options.required 39 | : false, 40 | choices: options.choices || undefined, 41 | options: Array.isArray(options.options) 42 | ? options.options.map(value => this.serializeOptions(value)) 43 | : undefined 44 | } 45 | } 46 | 47 | public async register (): Promise { 48 | const files = this.commandManager.factory.ignitor.files.filter((file: { type: string }) => file.type === 'command') 49 | const container = this.commandManager.factory.ignitor.container 50 | 51 | files.forEach((item) => { 52 | const instance = new item.default() 53 | const entityFile = new EntityFile(item.file.path) 54 | 55 | const command = new CommandEntity( 56 | undefined, 57 | instance.scope, 58 | instance.cooldown, 59 | instance.ctx, 60 | instance.run, 61 | entityFile 62 | ) 63 | 64 | container.commands.push(command) 65 | container.providers.forEach((provider: ProviderEntity) => { 66 | provider.load(command) 67 | }) 68 | }) 69 | 70 | const commands = container.commands.map((item) => item.ctx) 71 | await Promise.all( 72 | this.commandManager.factory.client!.guilds.cache.map(async (guild: Guild) => { 73 | return guild.commands.set(commands) 74 | }) 75 | ) 76 | 77 | this.commandManager.factory.client?.on('interactionCreate', async (command) => { 78 | if (command.isCommand()) { 79 | const commandEntity = container.commands.find((commandEntity) => command.commandName === commandEntity.ctx.name) 80 | 81 | if (!commandEntity) { 82 | return 83 | } 84 | await commandEntity?.run(command) 85 | } 86 | }) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApplicationCommandOption, ApplicationCommandOptionData, 3 | ApplicationCommandPermissionData, 4 | ApplicationCommandPermissions, 5 | ApplicationCommandPermissionsManager, 6 | Client, 7 | ClientEvents, 8 | Collection, 9 | GuildMember, 10 | Snowflake, 11 | VoiceState 12 | } from 'discord.js' 13 | import { EventEntity } from '../entities/Event' 14 | import { File } from 'fs-recursive' 15 | import { CommandEntity } from '../entities/Command' 16 | import { HookEntity } from '../entities/Hook' 17 | import { ProviderEntity } from '../entities/Provider' 18 | import Container from '../Container' 19 | import { ContextMenuEntity } from '../entities/ContextMenu' 20 | import { CAC } from 'cac' 21 | import Ignitor from '../Ignitor' 22 | 23 | export type ContainerType = 'event' | 'command' | 'hook' | 'middleware' | 'context-menu' 24 | 25 | export type HookType = 'application::starting' 26 | | 'application::ok' 27 | | 'application::client::login' 28 | | 'application::commands::registered' 29 | | 'application::events::registered' 30 | | 'application::hooks::registered' 31 | 32 | export type Constructable = { 33 | type: ContainerType 34 | default: any 35 | instance: HookEntity | EventEntity | CommandEntity 36 | file: File 37 | } 38 | 39 | export type ScopeContext = 'GLOBAL' | 'GUILDS' | Snowflake[] 40 | 41 | export type CommandContext = { 42 | name: string 43 | description: string 44 | options: ApplicationCommandOptionData[], 45 | defaultPermission?: boolean, 46 | } 47 | 48 | export type ApplicationContextOption = { 49 | name: string 50 | type: 'USER' | 'MESSAGE' 51 | defaultPermission?: boolean, 52 | } 53 | 54 | export type CommandGlobalContext = { 55 | scope: ScopeContext 56 | cooldown?: Cooldown, 57 | options: CommandContext 58 | } 59 | 60 | type NativeResolvable = { [K: string]: string | number | boolean | NativeResolvable } 61 | 62 | export type CliContextRuntime = { 63 | options: NativeResolvable, 64 | args: NativeResolvable, 65 | cli: CAC, 66 | ignitor: Ignitor 67 | } 68 | 69 | export type CliOption = { 70 | name: string 71 | description: string 72 | config?: { 73 | default?: any, 74 | type?: any[] 75 | } 76 | } 77 | 78 | export type CliCommandContext = { 79 | prefix: string, 80 | description: string 81 | args?: string[], 82 | config?: { 83 | allowUnknownOptions?: boolean, 84 | ignoreOptionDefaultValue?: boolean 85 | } 86 | options?: CliOption[] 87 | alias?: string[], 88 | exemple?: string 89 | } 90 | 91 | export type ApplicationGlobalContext = { 92 | scope: ScopeContext 93 | cooldown?: Cooldown, 94 | permissions?: ApplicationCommandPermissionData[] 95 | options: ApplicationContextOption 96 | } 97 | 98 | export type CommandContainer = CommandEntity[] 99 | 100 | export type EventContainer = EventEntity[] 101 | 102 | export type HookContainer = HookEntity[] 103 | 104 | export type ProviderContainer = ProviderEntity[] 105 | 106 | export type EntityResolvable = EventEntity | CommandEntity | ContextMenuEntity | HookEntity 107 | 108 | export type EnvironmentType = 'yaml' | 'yml' | 'json' 109 | 110 | export interface AddonContext { 111 | addon: Addon 112 | client: Client 113 | getModuleEnvironment (module: string, key: string): string 114 | getSelectEnvironment (): EnvironmentType 115 | getEnvironment (key: string): unknown | undefined 116 | getContainer (): Container 117 | getFiles (): Collection 118 | } 119 | 120 | export type Cooldown = { 121 | time?: number 122 | count?: number 123 | message?: string | null 124 | } 125 | 126 | export interface Events extends ClientEvents { 127 | guildMemberAddBoost: [member: GuildMember] 128 | guildMemberRemoveBoost: [member: GuildMember] 129 | voiceJoin: [state: VoiceState] 130 | voiceLeave: [state: VoiceState] 131 | websocketDebug: [payload: any] 132 | } 133 | 134 | export type CooldownActions = { 135 | timeout: any, 136 | count: number 137 | } 138 | 139 | export interface AddApplicationCommandPermissionsOptions { 140 | command: Snowflake 141 | permissions: {id: Snowflake, type: 'ROLE' | 'USER', permission: boolean }[] 142 | } 143 | 144 | export interface PermissionManagerResolvable extends ApplicationCommandPermissionsManager { 145 | add (options: AddApplicationCommandPermissionsOptions): Promise 146 | } -------------------------------------------------------------------------------- /src/utils/ConsoleColors.ts: -------------------------------------------------------------------------------- 1 | export enum Color { 2 | Reset = '\x1B[0m', 3 | Bright = '\x1B[1m', 4 | Dim = '\x1B[2m', 5 | Underscore = '\x1B[4m', 6 | Blink = '\x1B[5m', 7 | Reverse = '\x1B[7m', 8 | Hidden = '\x1B[8m', 9 | 10 | Black = '\x1B[30m', 11 | Red = '\x1B[31m', 12 | Green = '\x1B[32m', 13 | Yellow = '\x1B[33m', 14 | Blue = '\x1B[34m', 15 | Magenta = '\x1B[35m', 16 | Cyan = '\x1B[36m', 17 | White = '\x1B[37m', 18 | 19 | BgBlack = '\x1B[40m', 20 | BgRed = '\x1B[41m', 21 | BgGreen = '\x1B[42m', 22 | BgYellow = '\x1B[43m', 23 | BgBlue = '\x1B[44m', 24 | BgMagenta = '\x1B[45m', 25 | BgCyan = '\x1B[46m', 26 | BgWhite = '\x1B[47m', 27 | } -------------------------------------------------------------------------------- /src/utils/Constructable.ts: -------------------------------------------------------------------------------- 1 | import { ClientEvents } from 'discord.js' 2 | import { File } from 'fs-recursive' 3 | 4 | export default class Constructable { 5 | constructor (public file?: File | { path: string }) { 6 | if (this.file) { 7 | this.file = { 8 | ...file, 9 | path: file!.path 10 | .replace('\\build', '') 11 | .replace('.js', '.ts'), 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/utils/Cooldown.ts: -------------------------------------------------------------------------------- 1 | import { Cooldown as CooldownInterface, CooldownActions } from '../types' 2 | import { Collection, CommandInteraction, Snowflake } from 'discord.js' 3 | 4 | export default class Cooldown { 5 | private interaction!: CommandInteraction 6 | public readonly time: number 7 | public readonly count: number 8 | public readonly message?: string | null 9 | public memberMap: Collection = new Collection() 10 | 11 | constructor (options: CooldownInterface,) { 12 | this.message = options.message 13 | this.time = options.time 14 | ? options.time 15 | : 0 16 | 17 | if (options.count && !options.time) { 18 | throw new Error('The "count" parameter cannot be used without defining the "time" parameter') 19 | } 20 | this.count = options.count || 1 21 | } 22 | 23 | public setInteraction (commandInteraction: CommandInteraction) { 24 | this.interaction = commandInteraction 25 | } 26 | 27 | public async verify () { 28 | if (this.time) { 29 | return await this.addToMap() 30 | } 31 | return true 32 | } 33 | 34 | private async addToMap (): Promise { 35 | const member = this.interaction.member 36 | const targetMember = this.memberMap.get(member!.user.id) 37 | 38 | if (!targetMember) { 39 | this.memberMap.set(member!.user.id, { 40 | count: 1, 41 | timeout: setTimeout(() => { 42 | this.memberMap.delete(member!.user.id) 43 | }, this.time) 44 | }) 45 | return this.count >= 1 46 | } 47 | 48 | if (this.count && targetMember.count >= this.count && this.message !== null) { 49 | await this.interaction.reply({ 50 | content: this.message || `You have reached maximum usage, please try again later.`, 51 | ephemeral: true, 52 | }) 53 | return false 54 | } 55 | 56 | targetMember.count = targetMember.count + 1 57 | 58 | return true 59 | } 60 | } -------------------------------------------------------------------------------- /src/utils/EntityFile.ts: -------------------------------------------------------------------------------- 1 | import { File } from 'fs-recursive' 2 | import path from 'path' 3 | 4 | export default class EntityFile extends File { 5 | public relativePath: string 6 | constructor (location: string) { 7 | super(location) 8 | this.relativePath = location.replace(process.cwd(), '').replace(path.sep, '') 9 | } 10 | } -------------------------------------------------------------------------------- /src/utils/NodeEmitter.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events' 2 | 3 | class NodeEmitter extends EventEmitter { 4 | 5 | } 6 | 7 | export default new NodeEmitter() -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import Logger from '@leadcodedev/logger' 2 | import { MessageComponentInteraction } from 'discord.js' 3 | import CliTable from 'cli-table2' 4 | 5 | export const catchPromise = (error) => { 6 | Logger.send('error', error.message) 7 | process.exit(1) 8 | } 9 | 10 | const isObject = (param: any): boolean => Object.prototype.toString.call(param) === "[object Object]" 11 | 12 | const compareValue = (a: any, b: any, prop: string | number): boolean => { 13 | if (isObject(a[prop]) && isObject(b[prop])) { 14 | if (!isObjectEquivalent(a[prop], b[prop])) { 15 | return false 16 | } 17 | } else if (Array.isArray(a[prop]) && Array.isArray(b[prop])) { 18 | if (!isArrayEquivalent(a[prop], b[prop])) { 19 | return false 20 | } 21 | } else if (a[prop] !== b[prop]) { 22 | return false 23 | } 24 | 25 | return true 26 | } 27 | 28 | const isArrayEquivalent = (a: Array, b: Array): boolean => { 29 | if (!a || !b || !Array.isArray(a) || !Array.isArray(b)) { 30 | return false 31 | } 32 | 33 | if (a.length !== b.length) { 34 | return false 35 | } 36 | 37 | for (let i = 0; i < a.length; i++) { 38 | if (!compareValue(a, b, i)) return false 39 | } 40 | 41 | return true 42 | } 43 | 44 | const isObjectEquivalent = (a: Object, b: Object): boolean => { 45 | if (!a || !b || !isObject(a) || !isObject(b)) { 46 | return false 47 | } 48 | 49 | const aProps = Object.getOwnPropertyNames(a) 50 | const bProps = Object.getOwnPropertyNames(b) 51 | 52 | if (aProps.length !== bProps.length) { 53 | return false 54 | } 55 | 56 | for (let i = 0; i < aProps.length; i++) { 57 | const propName = aProps[i] 58 | 59 | if (!compareValue(a, b, propName)) { 60 | return false 61 | } 62 | } 63 | 64 | return true 65 | } 66 | 67 | export const isEquivalent = (a: any, b: any): boolean => { 68 | if (!a || !b) { 69 | return false 70 | } 71 | 72 | if (Array.isArray(a) || Array.isArray(b)) { 73 | return isArrayEquivalent(a, b) 74 | } 75 | 76 | if (isObject(a) || isObject(b)) { 77 | return isObjectEquivalent(a, b) 78 | } 79 | 80 | return a === b 81 | } 82 | 83 | export function emptyReply (interaction: MessageComponentInteraction) { 84 | (interaction.client as any).api.interactions(interaction.id, interaction.token).callback.post({ 85 | data: { 86 | type: 6, 87 | data: { 88 | flags: null, 89 | } 90 | } 91 | }) 92 | } 93 | 94 | export const table = new CliTable({ 95 | chars: { 96 | 'top-left': '╭', 97 | 'top-right': '╮', 98 | 'bottom-left': '╰', 99 | 'bottom-right': '╯', 100 | top: '─', 101 | bottom: '─', 102 | left: '│', 103 | right: '│', 104 | }, 105 | rowAligns: ['center', 'center'], 106 | }) 107 | 108 | export function isUsingYarn () { 109 | return (process.env.npm_config_user_agent || '').indexOf('yarn') === 0; 110 | } -------------------------------------------------------------------------------- /test/index.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | test ('test', (t) => { 4 | t.pass() 5 | }) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | "noImplicitAny": false, 5 | "target": "ESNext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 7 | "declaration": true /* Generates corresponding '.d.ts' file. */, /* Concatenate and emit output to single file. */ 8 | "outDir": "./build" /* Redirect output structure to the directory. */, 9 | "rootDir": "./src", 10 | "strict": true /* Enable all strict types-checking options. */, 11 | "baseUrl": "./src" 12 | /* Base directory to resolve non-absolute module names. */, 13 | "types": ["@types/node"] /* types declaration files to be included in compilation. */, 14 | "typeRoots": ["./contracts/**/*.d.ts"], 15 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 16 | "skipLibCheck": true /* Skip types checking of declaration files. */, 17 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, 18 | "experimentalDecorators": true, 19 | "resolveJsonModule": true, 20 | }, 21 | "include": ["src"], 22 | "exclude": [ 23 | "build", 24 | "tests" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------