Initial commit
This commit is contained in:
commit
1c07905f21
112 changed files with 11839 additions and 0 deletions
11
.dockerignore
Normal file
11
.dockerignore
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Folders
|
||||
/.cache
|
||||
/.git
|
||||
/dist
|
||||
/docs
|
||||
/misc
|
||||
/node_modules
|
||||
/temp
|
||||
|
||||
# Files
|
||||
/npm-debug.log
|
||||
8
.eslintignore
Normal file
8
.eslintignore
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Folders
|
||||
/.cache
|
||||
/.git
|
||||
/dist
|
||||
/docs
|
||||
/misc
|
||||
/node_modules
|
||||
/temp
|
||||
94
.eslintrc.json
Normal file
94
.eslintrc.json
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"plugins": ["@typescript-eslint", "import", "unicorn"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
"plugin:import/recommended",
|
||||
"plugin:import/typescript"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/explicit-function-return-type": [
|
||||
"error",
|
||||
{
|
||||
"allowExpressions": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-floating-promises": "off",
|
||||
"@typescript-eslint/no-inferrable-types": [
|
||||
"error",
|
||||
{
|
||||
"ignoreParameters": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-misused-promises": "off",
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/no-unsafe-return": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"@typescript-eslint/require-await": "off",
|
||||
"@typescript-eslint/restrict-template-expressions": "off",
|
||||
"@typescript-eslint/return-await": ["error", "always"],
|
||||
"@typescript-eslint/typedef": [
|
||||
"error",
|
||||
{
|
||||
"parameter": true,
|
||||
"propertyDeclaration": true
|
||||
}
|
||||
],
|
||||
"import/extensions": ["error", "ignorePackages"],
|
||||
"import/no-extraneous-dependencies": "error",
|
||||
"import/no-unresolved": "off",
|
||||
"import/no-useless-path-segments": "error",
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"alphabetize": {
|
||||
"caseInsensitive": true,
|
||||
"order": "asc"
|
||||
},
|
||||
"groups": [
|
||||
["builtin", "external", "object", "type"],
|
||||
["internal", "parent", "sibling", "index"]
|
||||
],
|
||||
"newlines-between": "always"
|
||||
}
|
||||
],
|
||||
"no-return-await": "off",
|
||||
"no-unused-vars": "off",
|
||||
"prefer-const": "off",
|
||||
"quotes": [
|
||||
"error",
|
||||
"single",
|
||||
{
|
||||
"allowTemplateLiterals": true
|
||||
}
|
||||
],
|
||||
"sort-imports": [
|
||||
"error",
|
||||
{
|
||||
"allowSeparatedGroups": true,
|
||||
"ignoreCase": true,
|
||||
"ignoreDeclarationSort": true,
|
||||
"ignoreMemberSort": false,
|
||||
"memberSyntaxSortOrder": ["none", "all", "multiple", "single"]
|
||||
}
|
||||
],
|
||||
"unicorn/prefer-node-protocol": "error"
|
||||
}
|
||||
}
|
||||
1
.gitbook.yaml
Normal file
1
.gitbook.yaml
Normal file
|
|
@ -0,0 +1 @@
|
|||
root: ./docs/
|
||||
129
.gitignore
vendored
Normal file
129
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
# =====================================================
|
||||
# Custom
|
||||
# =====================================================
|
||||
/dist
|
||||
/temp
|
||||
/**/config/**/*.json
|
||||
!/**/config/**/*.example.json
|
||||
|
||||
# =====================================================
|
||||
# Node.js
|
||||
# =====================================================
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
.env.production
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
8
.prettierignore
Normal file
8
.prettierignore
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Folders
|
||||
/.cache
|
||||
/.git
|
||||
/dist
|
||||
/docs
|
||||
/misc
|
||||
/node_modules
|
||||
/temp
|
||||
12
.prettierrc.json
Normal file
12
.prettierrc.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"printWidth": 100,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "as-needed",
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid",
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"streetsidesoftware.code-spell-checker"
|
||||
]
|
||||
}
|
||||
138
.vscode/launch.json
vendored
Normal file
138
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "start:bot",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"protocol": "inspector",
|
||||
"preLaunchTask": "build",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"runtimeExecutable": "node",
|
||||
"args": ["--enable-source-maps", "${workspaceFolder}/dist/start-bot.js"],
|
||||
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"],
|
||||
"outputCapture": "std",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"restart": false
|
||||
},
|
||||
{
|
||||
"name": "start:manager",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"protocol": "inspector",
|
||||
"preLaunchTask": "build",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"runtimeExecutable": "node",
|
||||
"args": ["--enable-source-maps", "${workspaceFolder}/dist/start-manager.js"],
|
||||
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"],
|
||||
"outputCapture": "std",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"restart": false
|
||||
},
|
||||
{
|
||||
"name": "commands:view",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"protocol": "inspector",
|
||||
"preLaunchTask": "build",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"runtimeExecutable": "node",
|
||||
"args": [
|
||||
"--enable-source-maps",
|
||||
"${workspaceFolder}/dist/start-bot.js",
|
||||
"commands",
|
||||
"view"
|
||||
],
|
||||
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"],
|
||||
"outputCapture": "std",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"restart": false
|
||||
},
|
||||
{
|
||||
"name": "commands:register",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"protocol": "inspector",
|
||||
"preLaunchTask": "build",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"runtimeExecutable": "node",
|
||||
"args": [
|
||||
"--enable-source-maps",
|
||||
"${workspaceFolder}/dist/start-bot.js",
|
||||
"commands",
|
||||
"register"
|
||||
],
|
||||
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"],
|
||||
"outputCapture": "std",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"restart": false
|
||||
},
|
||||
{
|
||||
"name": "commands:rename",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"protocol": "inspector",
|
||||
"preLaunchTask": "build",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"runtimeExecutable": "node",
|
||||
"args": [
|
||||
"--enable-source-maps",
|
||||
"${workspaceFolder}/dist/start-bot.js",
|
||||
"commands",
|
||||
"rename",
|
||||
"old_name",
|
||||
"new_name"
|
||||
],
|
||||
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"],
|
||||
"outputCapture": "std",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"restart": false
|
||||
},
|
||||
{
|
||||
"name": "commands:delete",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"protocol": "inspector",
|
||||
"preLaunchTask": "build",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"runtimeExecutable": "node",
|
||||
"args": [
|
||||
"--enable-source-maps",
|
||||
"${workspaceFolder}/dist/start-bot.js",
|
||||
"commands",
|
||||
"delete",
|
||||
"command_name"
|
||||
],
|
||||
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"],
|
||||
"outputCapture": "std",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"restart": false
|
||||
},
|
||||
{
|
||||
"name": "commands:clear",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"protocol": "inspector",
|
||||
"preLaunchTask": "build",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"runtimeExecutable": "node",
|
||||
"args": [
|
||||
"--enable-source-maps",
|
||||
"${workspaceFolder}/dist/start-bot.js",
|
||||
"commands",
|
||||
"clear"
|
||||
],
|
||||
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"],
|
||||
"outputCapture": "std",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"restart": false
|
||||
}
|
||||
]
|
||||
}
|
||||
44
.vscode/settings.json
vendored
Normal file
44
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"cSpell.enabled": true,
|
||||
"cSpell.words": [
|
||||
"autocompletes",
|
||||
"autocompleting",
|
||||
"bot's",
|
||||
"cmds",
|
||||
"cooldown",
|
||||
"cooldowns",
|
||||
"datas",
|
||||
"descs",
|
||||
"discordbotlist",
|
||||
"discordjs",
|
||||
"discordlabs",
|
||||
"discordlist",
|
||||
"disforge",
|
||||
"filesize",
|
||||
"luxon",
|
||||
"millis",
|
||||
"Novak",
|
||||
"ondiscord",
|
||||
"parens",
|
||||
"pino",
|
||||
"regexes",
|
||||
"respawn",
|
||||
"respawned",
|
||||
"restjson",
|
||||
"unescapes",
|
||||
"varchar"
|
||||
],
|
||||
"typescript.preferences.importModuleSpecifierEnding": "js"
|
||||
}
|
||||
19
.vscode/tasks.json
vendored
Normal file
19
.vscode/tasks.json
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build",
|
||||
"type": "shell",
|
||||
"command": "${workspaceFolder}\\node_modules\\.bin\\tsc",
|
||||
"args": ["--project", "${workspaceFolder}\\tsconfig.json"]
|
||||
}
|
||||
],
|
||||
"windows": {
|
||||
"options": {
|
||||
"shell": {
|
||||
"executable": "cmd.exe",
|
||||
"args": ["/d", "/c"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
Dockerfile
Normal file
22
Dockerfile
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
FROM node:16
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package.json and package-lock.json
|
||||
COPY package*.json ./
|
||||
|
||||
# Install packages
|
||||
RUN npm install
|
||||
|
||||
# Copy the app code
|
||||
COPY . .
|
||||
|
||||
# Build the project
|
||||
RUN npm run build
|
||||
|
||||
# Expose ports
|
||||
EXPOSE 3001
|
||||
|
||||
# Run the application
|
||||
CMD [ "node", "dist/start-manager.js" ]
|
||||
76
LEGAL.md
Normal file
76
LEGAL.md
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# Terms of Service
|
||||
|
||||
## Usage Agreement
|
||||
|
||||
By inviting the bot or using its features, you are agreeing to the below mentioned Terms of Service and Privacy Policy.
|
||||
|
||||
You acknowledge that you have the privilege to use the bot freely on any Discord server you share with it, that you can invite it to any server that you have "Manage Server" rights for and that this privilege might get revoked for you, if you're subject of breaking the terms and/or policy of this bot, or the Terms of Service, Privacy Policy and/or Community Guidelines of Discord Inc.
|
||||
|
||||
Through inviting or interacting with the bot it may collect specific data as described in its [Privacy Policy](#privacy-policy). The intended usage of this data is for core functionalities of the bot such as command handling, server settings, and user settings.
|
||||
|
||||
## Intended Age
|
||||
|
||||
The bot may not be used by individuals under the minimal age described in Discord's Terms of Service.
|
||||
|
||||
Do not provide any age-restricted content (as defined in Discord's safety policies) to the bot. Age-restricted content includes but is not limited to content and discussion related to:
|
||||
|
||||
- Sexually explicit material such as pornography or sexually explicit text
|
||||
- Violent content
|
||||
- Illegal, dangerous, and regulated goods such as firearms, tactical gear, alcohol, or drug use
|
||||
- Gambling-adjacent or addictive behavior
|
||||
|
||||
Content submitted to the bot through the use of commands arguments, text inputs, image inputs, or otherwise must adhere to the above conditions. Violating these conditions may result in your account being reported to Discord Inc for further action.
|
||||
|
||||
## Affiliation
|
||||
|
||||
The bot is not affiliated with, supported by, or made by Discord Inc.
|
||||
|
||||
Any direct connection to Discord or any of its trademark objects is purely coincidental. We do not claim to have the copyright ownership of any of Discord's assets, trademarks or other intellectual property.
|
||||
|
||||
## Liability
|
||||
|
||||
The owner(s) of the bot may not be made liable for individuals breaking these Terms at any given time. We have faith in the end users being truthful about their information and not misusing this bot or the services of Discord Inc in a malicious way.
|
||||
|
||||
We reserve the right to update these terms at our own discretion, giving you a 1-week (7 days) period to opt out of these terms if you're not agreeing with the new changes.
|
||||
|
||||
You may opt out by removing the bot from any server you have the rights for.
|
||||
|
||||
## Contact
|
||||
|
||||
People may get in contact through the official support server of the bot.
|
||||
|
||||
Other ways of support may be provided but aren't guaranteed.
|
||||
|
||||
# Privacy Policy
|
||||
|
||||
## Usage of Data
|
||||
|
||||
The bot may use stored data, as defined below, for different features including but not limited to:
|
||||
|
||||
- Command handling
|
||||
- Providing server and user preferences
|
||||
|
||||
The bot may share non-sensitive data with 3rd party sites or services, including but not limited to:
|
||||
|
||||
- Aggregate/statistical data (ex: total number of server or users)
|
||||
- Discord generated IDs needed to tie 3rd party data to Discord or user-provided data
|
||||
|
||||
Personally identifiable (other than IDs) or sensitive information will not be shared with 3rd party sites or services.
|
||||
|
||||
## Updating Data
|
||||
|
||||
The bot's data may be updated when using specific commands.
|
||||
|
||||
Updating data can require the input of an end user, and data that can be seen as sensitive, such as content of a message, may need to be stored when using certain commands.
|
||||
|
||||
## Temporarily Stored Data
|
||||
|
||||
The bot may keep stored data in an internal caching mechanic for a certain amount of time. After this time period, the cached information will be dropped and only be re-added when required.
|
||||
|
||||
Data may be dropped from cache pre-maturely through actions such as removing the bot from the server.
|
||||
|
||||
## Removal of Data
|
||||
|
||||
Manual removal of the data can be requested through the official support server. Discord IDs such as user, guild, role, etc. may be stored even after the removal of other data in order to properly identify bot specific statistics since those IDs are public and non-sensitive.
|
||||
|
||||
For security reasons we will ask you to provide us with proof of ownership to the data you wish to be removed. Only a server owner may request manual removal of server data.
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Kevin Novak
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
133
README.md
Normal file
133
README.md
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# Discord Bot TypeScript Template
|
||||
|
||||
[](https://discord.js.org/)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://github.com/KevinNovak/Discord-Bot-TypeScript-Template/stargazers)
|
||||
[](https://github.com/KevinNovak/Discord-Bot-TypeScript-Template/pulls)
|
||||
|
||||
**Discord bot** - A discord.js bot template written with TypeScript.
|
||||
|
||||
## Introduction
|
||||
|
||||
This template was created to give developers a starting point for new Discord bots, so that much of the initial setup can be avoided and developers can instead focus on meaningful bot features. Developers can simply copy this repo, follow the [setup instructions](#setup) below, and have a working bot with many [boilerplate features](#features) already included!
|
||||
|
||||
For help using this template, feel free to [join our support server](https://discord.gg/c9kQktCbsE)!
|
||||
|
||||
[](https://discord.gg/c9kQktCbsE)
|
||||
|
||||
## Features
|
||||
|
||||
### Built-In Bot Features:
|
||||
|
||||
- Basic command structure.
|
||||
- Rate limits and command cooldowns.
|
||||
- Welcome message when joining a server.
|
||||
- Shows server count in bot status.
|
||||
- Posts server count to popular bot list websites.
|
||||
- Support for multiple languages.
|
||||
|
||||
### Developer Friendly:
|
||||
|
||||
- Written with TypeScript.
|
||||
- Uses the [discord.js](https://discord.js.org/) framework.
|
||||
- Built-in debugging setup for VSCode.
|
||||
- Written with [ESM](https://nodejs.org/api/esm.html#introduction) for future compatibility with packages.
|
||||
- Support for running with the [PM2](https://pm2.keymetrics.io/) process manger.
|
||||
- Support for running with [Docker](https://www.docker.com/).
|
||||
|
||||
### Scales as Your Bot Grows:
|
||||
|
||||
- Supports [sharding](https://discordjs.guide/sharding/) which is required when your bot is in 2500+ servers.
|
||||
- Supports [clustering](https://github.com/KevinNovak/Discord-Bot-TypeScript-Template-Master-Api) which allows you to run your bot on multiple machines.
|
||||
|
||||
## Commands
|
||||
|
||||
This bot has a few example commands which can be modified as needed.
|
||||
|
||||
### Help Command
|
||||
|
||||
A `/help` command to get help on different areas of the bot or to contact support:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### Info Command
|
||||
|
||||
A `/info` command to get information about the bot or links to different resources.
|
||||
|
||||

|
||||
|
||||
### Test Command
|
||||
|
||||
A generic command, `/test`, which can be copied to create additional commands.
|
||||
|
||||

|
||||
|
||||
### Dev Command
|
||||
|
||||
A `/dev` command which can only be run by the bot developer. Shows developer information, but can be extended to perform developer-only actions.
|
||||
|
||||

|
||||
|
||||
### Welcome Message
|
||||
|
||||
A welcome message is sent to the server and owner when the bot is added.
|
||||
|
||||

|
||||
|
||||
## Setup
|
||||
|
||||
1. Copy example config files.
|
||||
- Navigate to the `config` folder of this project.
|
||||
- Copy all files ending in `.example.json` and remove the `.example` from the copied file names.
|
||||
- Ex: `config.example.json` should be copied and renamed as `config.json`.
|
||||
2. Obtain a bot token.
|
||||
- You'll need to create a new bot in your [Discord Developer Portal](https://discord.com/developers/applications/).
|
||||
- See [here](https://www.writebots.com/discord-bot-token/) for detailed instructions.
|
||||
- At the end you should have a **bot token**.
|
||||
3. Modify the config file.
|
||||
- Open the `config/config.json` file.
|
||||
- You'll need to edit the following values:
|
||||
- `client.id` - Your discord bot's [user ID](https://techswift.org/2020/04/22/how-to-find-your-user-id-on-discord/).
|
||||
- `client.token` - Your discord bot's token.
|
||||
4. Install packages.
|
||||
- Navigate into the downloaded source files and type `npm install`.
|
||||
5. Register commands.
|
||||
- In order to use slash commands, they first [have to be registered](https://discordjs.guide/creating-your-bot/command-deployment.html).
|
||||
- Type `npm run commands:register` to register the bot's commands.
|
||||
- Run this script any time you change a command name, structure, or add/remove commands.
|
||||
- This is so Discord knows what your commands look like.
|
||||
- It may take up to an hour for command changes to appear.
|
||||
|
||||
## Start Scripts
|
||||
|
||||
You can run the bot in multiple modes:
|
||||
|
||||
1. Normal Mode
|
||||
- Type `npm start`.
|
||||
- Starts a single instance of the bot.
|
||||
2. Manager Mode
|
||||
- Type `npm run start:manager`.
|
||||
- Starts a shard manager which will spawn multiple bot shards.
|
||||
3. PM2 Mode
|
||||
- Type `npm run start:pm2`.
|
||||
- Similar to Manager Mode but uses [PM2](https://pm2.keymetrics.io/) to manage processes.
|
||||
|
||||
## Bots Using This Template
|
||||
|
||||
A list of Discord bots using this template.
|
||||
|
||||
| Bot | Servers |
|
||||
| ---------------------------------------------------------------------- | ------------------------------------------------------------- |
|
||||
| [Birthday Bot](https://top.gg/bot/656621136808902656) |  |
|
||||
| [QOTD Bot](https://top.gg/bot/713586207119900693) |  |
|
||||
| [Friend Time](https://top.gg/bot/471091072546766849) |  |
|
||||
| [Bento](https://top.gg/bot/787041583580184609) |  |
|
||||
| [NFT-Info](https://top.gg/bot/902249456072818708) |  |
|
||||
| [Skylink-IF](https://top.gg/bot/929527099922993162) |  |
|
||||
| [Topcoder TC-101](https://github.com/topcoder-platform/tc-discord-bot) | |
|
||||
|
||||
Don't see your bot listed? [Contact us](https://discord.gg/c9kQktCbsE) to have your bot added!
|
||||
44
config/bot-sites.example.json
Normal file
44
config/bot-sites.example.json
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
[
|
||||
{
|
||||
"name": "top.gg",
|
||||
"enabled": false,
|
||||
"url": "https://top.gg/api/bots/<BOT_ID>/stats",
|
||||
"authorization": "<TOKEN>",
|
||||
"body": "{\"server_count\":{{SERVER_COUNT}}}"
|
||||
},
|
||||
{
|
||||
"name": "bots.ondiscord.xyz",
|
||||
"enabled": false,
|
||||
"url": "https://bots.ondiscord.xyz/bot-api/bots/<BOT_ID>/guilds",
|
||||
"authorization": "<TOKEN>",
|
||||
"body": "{\"guildCount\":{{SERVER_COUNT}}}"
|
||||
},
|
||||
{
|
||||
"name": "discord.bots.gg",
|
||||
"enabled": false,
|
||||
"url": "https://discord.bots.gg/api/v1/bots/<BOT_ID>/stats",
|
||||
"authorization": "<TOKEN>",
|
||||
"body": "{\"guildCount\":{{SERVER_COUNT}}}"
|
||||
},
|
||||
{
|
||||
"name": "discordbotlist.com",
|
||||
"enabled": false,
|
||||
"url": "https://discordbotlist.com/api/bots/<BOT_ID>/stats",
|
||||
"authorization": "Bot <TOKEN>",
|
||||
"body": "{\"guilds\":{{SERVER_COUNT}}}"
|
||||
},
|
||||
{
|
||||
"name": "discords.com",
|
||||
"enabled": false,
|
||||
"url": "https://discords.com/bots/api/bot/<BOT_ID>",
|
||||
"authorization": "<TOKEN>",
|
||||
"body": "{\"server_count\":{{SERVER_COUNT}}}"
|
||||
},
|
||||
{
|
||||
"name": "disforge.com",
|
||||
"enabled": false,
|
||||
"url": "https://disforge.com/api/botstats/<BOT_ID>",
|
||||
"authorization": "<TOKEN>",
|
||||
"body": "{\"servers\":{{SERVER_COUNT}}}"
|
||||
}
|
||||
]
|
||||
80
config/config.example.json
Normal file
80
config/config.example.json
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
{
|
||||
"developers": ["<YOUR_DISCORD_ID>"],
|
||||
"client": {
|
||||
"id": "<DISCORD_BOT_ID>",
|
||||
"token": "<DISCORD_BOT_TOKEN>",
|
||||
"intents": [
|
||||
"Guilds",
|
||||
"GuildMessages",
|
||||
"GuildMessageReactions",
|
||||
"DirectMessages",
|
||||
"DirectMessageReactions"
|
||||
],
|
||||
"partials": ["Message", "Channel", "Reaction"],
|
||||
"caches": {
|
||||
"AutoModerationRuleManager": 0,
|
||||
"BaseGuildEmojiManager": 0,
|
||||
"GuildEmojiManager": 0,
|
||||
"GuildBanManager": 0,
|
||||
"GuildInviteManager": 0,
|
||||
"GuildScheduledEventManager": 0,
|
||||
"GuildStickerManager": 0,
|
||||
"MessageManager": 0,
|
||||
"PresenceManager": 0,
|
||||
"StageInstanceManager": 0,
|
||||
"ThreadManager": 0,
|
||||
"ThreadMemberManager": 0,
|
||||
"VoiceStateManager": 0
|
||||
}
|
||||
},
|
||||
"api": {
|
||||
"port": 3001,
|
||||
"secret": "00000000-0000-0000-0000-000000000000"
|
||||
},
|
||||
"sharding": {
|
||||
"spawnDelay": 5,
|
||||
"spawnTimeout": 300,
|
||||
"serversPerShard": 1000
|
||||
},
|
||||
"clustering": {
|
||||
"enabled": false,
|
||||
"shardCount": 16,
|
||||
"callbackUrl": "http://localhost:3001/",
|
||||
"masterApi": {
|
||||
"url": "http://localhost:5000/",
|
||||
"token": "00000000-0000-0000-0000-000000000000"
|
||||
}
|
||||
},
|
||||
"jobs": {
|
||||
"updateServerCount": {
|
||||
"schedule": "0 */10 * * * *",
|
||||
"log": false,
|
||||
"runOnce": false,
|
||||
"initialDelaySecs": 0
|
||||
}
|
||||
},
|
||||
"rateLimiting": {
|
||||
"commands": {
|
||||
"amount": 10,
|
||||
"interval": 30
|
||||
},
|
||||
"buttons": {
|
||||
"amount": 10,
|
||||
"interval": 30
|
||||
},
|
||||
"triggers": {
|
||||
"amount": 10,
|
||||
"interval": 30
|
||||
},
|
||||
"reactions": {
|
||||
"amount": 10,
|
||||
"interval": 30
|
||||
}
|
||||
},
|
||||
"logging": {
|
||||
"pretty": true,
|
||||
"rateLimit": {
|
||||
"minTimeout": 30
|
||||
}
|
||||
}
|
||||
}
|
||||
12
config/debug.example.json
Normal file
12
config/debug.example.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"override": {
|
||||
"shardMode": {
|
||||
"enabled": false,
|
||||
"value": "worker"
|
||||
}
|
||||
},
|
||||
"dummyMode": {
|
||||
"enabled": false,
|
||||
"whitelist": ["212772875793334272", "478288246858711040"]
|
||||
}
|
||||
}
|
||||
37
lang/lang.common.json
Normal file
37
lang/lang.common.json
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"bot": {
|
||||
"name": "My Bot",
|
||||
"author": "My Name"
|
||||
},
|
||||
"emojis": {
|
||||
"yes": "✅",
|
||||
"no": "❌",
|
||||
"enabled": "🟢",
|
||||
"disabled": "🔴",
|
||||
"info": "ℹ️",
|
||||
"warning": "⚠️",
|
||||
"previous": "◀️",
|
||||
"next": "▶️",
|
||||
"first": "⏪",
|
||||
"last": "⏩",
|
||||
"refresh": "🔄"
|
||||
},
|
||||
"colors": {
|
||||
"default": "#0099ff",
|
||||
"success": "#00ff83",
|
||||
"warning": "#ffcc66",
|
||||
"error": "#ff4a4a"
|
||||
},
|
||||
"links": {
|
||||
"author": "https://github.com/",
|
||||
"docs": "https://top.gg/",
|
||||
"donate": "https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=EW389DYYSS4FC",
|
||||
"invite": "https://discord.com/",
|
||||
"source": "https://github.com/",
|
||||
"stream": "https://www.twitch.tv/novakevin",
|
||||
"support": "https://support.discord.com/",
|
||||
"template": "https://github.com/KevinNovak/Discord-Bot-TypeScript-Template",
|
||||
"terms": "https://github.com/KevinNovak/Discord-Bot-TypeScript-Template/blob/master/LEGAL.md#terms-of-service",
|
||||
"vote": "https://top.gg/"
|
||||
}
|
||||
}
|
||||
308
lang/lang.en-GB.json
Normal file
308
lang/lang.en-GB.json
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
{
|
||||
"data": {
|
||||
"displayEmbeds": {
|
||||
"welcome": {
|
||||
"title": "Thank you for using {{COM:bot.name}}!",
|
||||
"description": ["{{REF:bot.description}}"],
|
||||
"fields": [
|
||||
{
|
||||
"name": "Important {{REF:fields.commands}}",
|
||||
"value": ["{{CMD_LINK_HELP}} - {{REF:commandDescs.help}}"]
|
||||
},
|
||||
{
|
||||
"name": "{{REF:fields.links}}",
|
||||
"value": ["{{REF:links.docsEmbed}}", "{{REF:links.supportEmbed}}"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"helpContactSupport": {
|
||||
"title": "Help - {{REF:helpOptions.contactSupport}}",
|
||||
"description": [
|
||||
"Have a question or feedback? Join our support server at the link below!"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "{{REF:fields.links}}",
|
||||
"value": ["{{REF:links.supportEmbed}}"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"helpCommands": {
|
||||
"title": "Help - {{REF:helpOptions.commands}}",
|
||||
"fields": [
|
||||
{
|
||||
"name": "Commands",
|
||||
"value": [
|
||||
"To see the available commands, just type `/` and select the bot from the left side. You can then scroll through all available commands. Some commands may be hidden if you don't have permission to view them.",
|
||||
"",
|
||||
"{{CMD_LINK_TEST}} - {{REF:commandDescs.test}}",
|
||||
"{{CMD_LINK_INFO}} - {{REF:commandDescs.info}}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Command Permissions",
|
||||
"value": [
|
||||
"Want to restrict commands to certain roles, users, or channels? Set up permissions in the bot's integration page by going to **Server Settings** > **Integrations**, and then **Manage** for this bot."
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "{{REF:fields.links}}",
|
||||
"value": ["{{REF:links.docsEmbed}}", "{{REF:links.supportEmbed}}"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"test": {
|
||||
"description": "Test command works!"
|
||||
},
|
||||
"viewDateJoined": {
|
||||
"description": "{{TARGET}} joined on {{DATE}}!"
|
||||
},
|
||||
"viewDateSent": {
|
||||
"description": "This message was sent on {{DATE}}!"
|
||||
},
|
||||
"about": {
|
||||
"title": "{{COM:bot.name}} - About",
|
||||
"description": "{{REF:bot.description}}",
|
||||
"fields": [
|
||||
{ "name": "Author", "value": "{{REF:links.authorEmbed}}" },
|
||||
{
|
||||
"name": "{{REF:fields.links}}",
|
||||
"value": [
|
||||
"{{REF:links.sourceEmbed}}",
|
||||
"{{REF:links.docsEmbed}}",
|
||||
"{{REF:links.termsEmbed}}",
|
||||
"{{REF:links.voteEmbed}}",
|
||||
"{{REF:links.donateEmbed}}",
|
||||
"{{REF:links.supportEmbed}}",
|
||||
"{{REF:links.inviteEmbed}}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Created With",
|
||||
"value": ["{{REF:links.templateEmbed}}"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"translate": {
|
||||
"title": "{{COM:bot.name}} - Translations",
|
||||
"description": "Thank you to our translators who have made it possible for {{COM:bot.name}} to be used in the following languages. If you are interested in providing a translation, please contact the staff in our [support server]({{COM:links.support}})."
|
||||
},
|
||||
"devInfo": {
|
||||
"title": "{{COM:bot.name}} - Developer Info",
|
||||
"fields": [
|
||||
{
|
||||
"name": "Versions",
|
||||
"value": [
|
||||
"**Node.js**: {{NODE_VERSION}}",
|
||||
"**TypeScript**: {{TS_VERSION}}",
|
||||
"**ECMAScript**: {{ES_VERSION}}",
|
||||
"**discord.js**: {{DJS_VERSION}}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Stats",
|
||||
"value": [
|
||||
"**Shards**: {{SHARD_COUNT}}",
|
||||
"**Servers**: {{SERVER_COUNT}} ({{SERVER_COUNT_PER_SHARD}}/Shard)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Memory",
|
||||
"value": [
|
||||
"**RSS**: {{RSS_SIZE}} ({{RSS_SIZE_PER_SERVER}}/Server)",
|
||||
"**Heap**: {{HEAP_TOTAL_SIZE}} ({{HEAP_TOTAL_SIZE_PER_SERVER}}/Server)",
|
||||
"**Used**: {{HEAP_USED_SIZE}} ({{HEAP_USED_SIZE_PER_SERVER}}/Server)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "IDs",
|
||||
"value": [
|
||||
"**Hostname**: {{HOSTNAME}}",
|
||||
"**Shard ID**: {{SHARD_ID}}",
|
||||
"**Server ID**: {{SERVER_ID}}",
|
||||
"**Bot ID**: {{BOT_ID}}",
|
||||
"**User ID**: {{USER_ID}}"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"validationEmbeds": {
|
||||
"cooldownHit": {
|
||||
"description": "You can only run this command {{AMOUNT}} time(s) every {{INTERVAL}}. Please wait before attempting this command again.",
|
||||
"color": "{{COM:colors.warning}}"
|
||||
},
|
||||
"devOnly": {
|
||||
"description": "This action can only be done by developers.",
|
||||
"color": "{{COM:colors.warning}}"
|
||||
},
|
||||
"missingClientPerms": {
|
||||
"description": [
|
||||
"I don't have all permissions required to run that command here! Please check the server and channel permissions to make sure I have the following permissions.",
|
||||
"",
|
||||
"Required permissions: {{PERMISSIONS}}"
|
||||
],
|
||||
"color": "{{COM:colors.warning}}"
|
||||
}
|
||||
},
|
||||
"errorEmbeds": {
|
||||
"command": {
|
||||
"description": "Something went wrong!",
|
||||
"fields": [
|
||||
{
|
||||
"name": "Error code",
|
||||
"value": "{{ERROR_CODE}}"
|
||||
},
|
||||
{
|
||||
"name": "Server ID",
|
||||
"value": "{{GUILD_ID}}"
|
||||
},
|
||||
{
|
||||
"name": "Shard ID",
|
||||
"value": "{{SHARD_ID}}"
|
||||
},
|
||||
{
|
||||
"name": "Contact support",
|
||||
"value": "{{COM:links.support}}"
|
||||
}
|
||||
],
|
||||
"color": "{{COM:colors.error}}"
|
||||
},
|
||||
"startupInProcess": {
|
||||
"description": "{{COM:bot.name}} is still starting up. Try again later.",
|
||||
"color": "{{COM:colors.warning}}"
|
||||
},
|
||||
"notImplemented": {
|
||||
"description": "This feature has not been implemented yet!",
|
||||
"color": "{{COM:colors.warning}}"
|
||||
}
|
||||
},
|
||||
"channelRegexes": {
|
||||
"bot": "/bot|command|cmd/i"
|
||||
}
|
||||
},
|
||||
"refs": {
|
||||
"meta": {
|
||||
"translators": "[TranslatorName#1234](https://github.com/)"
|
||||
},
|
||||
"bot": {
|
||||
"description": "{{REF:links.templateEmbed}} helps give developers a starting point for new Discord bots, so that much of the initial setup can be avoided and developers can instead focus on meaningful bot features."
|
||||
},
|
||||
"chatCommands": {
|
||||
"dev": "dev",
|
||||
"help": "help",
|
||||
"info": "info",
|
||||
"test": "test"
|
||||
},
|
||||
"userCommands": {
|
||||
"viewDateJoined": "View Date Joined"
|
||||
},
|
||||
"messageCommands": {
|
||||
"viewDateSent": "View Date Sent"
|
||||
},
|
||||
"arguments": {
|
||||
"command": "command",
|
||||
"option": "option"
|
||||
},
|
||||
"commandDescs": {
|
||||
"dev": "Developer use only.",
|
||||
"help": "Find help or contact support.",
|
||||
"info": "View bot info.",
|
||||
"test": "Run the test command."
|
||||
},
|
||||
"argDescs": {
|
||||
"devCommand": "Command.",
|
||||
"helpOption": "Option.",
|
||||
"infoOption": "Option."
|
||||
},
|
||||
"fields": {
|
||||
"commands": "Commands",
|
||||
"links": "Links"
|
||||
},
|
||||
"permissions": {
|
||||
"AddReactions": "Add Reactions",
|
||||
"Administrator": "Administrator",
|
||||
"AttachFiles": "Attach Files",
|
||||
"BanMembers": "Ban Members",
|
||||
"ChangeNickname": "Change Nickname",
|
||||
"Connect": "Connect",
|
||||
"CreateInstantInvite": "Create Invite",
|
||||
"CreatePrivateThreads": "Create Private Threads",
|
||||
"CreatePublicThreads": "Create Public Threads",
|
||||
"DeafenMembers": "Deafen Members",
|
||||
"EmbedLinks": "Embed Links",
|
||||
"KickMembers": "Kick Members",
|
||||
"ManageChannels": "Manage Channel(s)",
|
||||
"ManageEmojisAndStickers": "Manage Emoji and Stickers",
|
||||
"ManageEvents": "Manage Events",
|
||||
"ManageGuild": "Manage Server",
|
||||
"ManageGuildExpressions": "Manage Expressions",
|
||||
"ManageMessages": "Manage Messages",
|
||||
"ManageNicknames": "Manage Nicknames",
|
||||
"ManageRoles": "Manage Roles / Permissions",
|
||||
"ManageThreads": "Manage Threads / Posts",
|
||||
"ManageWebhooks": "Manage Webhooks",
|
||||
"MentionEveryone": "Mention Everyone, Here, and All Roles",
|
||||
"ModerateMembers": "Timeout Members",
|
||||
"MoveMembers": "Move Members",
|
||||
"MuteMembers": "Mute Members",
|
||||
"PrioritySpeaker": "Priority Speaker",
|
||||
"ReadMessageHistory": "Read Message History",
|
||||
"RequestToSpeak": "Request to Speak",
|
||||
"SendMessages": "Send Messages / Create Posts",
|
||||
"SendMessagesInThreads": "Send Messages in Threads / Posts",
|
||||
"SendTTSMessages": "Send Text-to-Speech Messages",
|
||||
"SendVoiceMessages": "Send Voice Messages",
|
||||
"Speak": "Speak",
|
||||
"Stream": "Video",
|
||||
"UseApplicationCommands": "Use Application Commands",
|
||||
"UseEmbeddedActivities": "Use Activities",
|
||||
"UseExternalEmojis": "Use External Emoji",
|
||||
"UseExternalSounds": "Use External Sounds",
|
||||
"UseExternalStickers": "Use External Stickers",
|
||||
"UseSoundboard": "Use Soundboard",
|
||||
"UseVAD": "Use Voice Activity",
|
||||
"ViewAuditLog": "View Audit Log",
|
||||
"ViewChannel": "View Channel(s)",
|
||||
"ViewCreatorMonetizationAnalytics": "View Server Subscription Insights",
|
||||
"ViewGuildInsights": "View Server Insights"
|
||||
},
|
||||
"devCommandNames": {
|
||||
"info": "info"
|
||||
},
|
||||
"helpOptions": {
|
||||
"contactSupport": "Contact Support",
|
||||
"commands": "Commands"
|
||||
},
|
||||
"helpOptionDescs": {
|
||||
"contactSupport": "❓ {{REF:helpOptions.contactSupport}} ❓",
|
||||
"commands": "{{REF:helpOptions.commands}} -- What commands are there? How do I restrict who is allowed to use commands?"
|
||||
},
|
||||
"infoOptions": {
|
||||
"about": "About",
|
||||
"translate": "Translate"
|
||||
},
|
||||
"yesNo": {
|
||||
"yes": "Yes",
|
||||
"no": "No"
|
||||
},
|
||||
"boolean": {
|
||||
"true": "True",
|
||||
"false": "False"
|
||||
},
|
||||
"other": {
|
||||
"na": "N/A"
|
||||
},
|
||||
"links": {
|
||||
"authorEmbed": "[{{COM:bot.author}}]({{COM:links.author}})",
|
||||
"docsEmbed": "[View Documentation]({{COM:links.docs}})",
|
||||
"donateEmbed": "[Donate via PayPal]({{COM:links.donate}})",
|
||||
"inviteEmbed": "[Invite {{COM:bot.name}} to a Server!]({{COM:links.invite}})",
|
||||
"sourceEmbed": "[View Source Code]({{COM:links.source}})",
|
||||
"supportEmbed": "[Join Support Server]({{COM:links.support}})",
|
||||
"templateEmbed": "[Discord Bot TypeScript Template]({{COM:links.template}})",
|
||||
"termsEmbed": "[View Terms of Service]({{COM:links.terms}})",
|
||||
"voteEmbed": "[Vote for {{COM:bot.name}}!]({{COM:links.vote}})"
|
||||
}
|
||||
}
|
||||
}
|
||||
308
lang/lang.en-US.json
Normal file
308
lang/lang.en-US.json
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
{
|
||||
"data": {
|
||||
"displayEmbeds": {
|
||||
"welcome": {
|
||||
"title": "Thank you for using {{COM:bot.name}}!",
|
||||
"description": ["{{REF:bot.description}}"],
|
||||
"fields": [
|
||||
{
|
||||
"name": "Important {{REF:fields.commands}}",
|
||||
"value": ["{{CMD_LINK_HELP}} - {{REF:commandDescs.help}}"]
|
||||
},
|
||||
{
|
||||
"name": "{{REF:fields.links}}",
|
||||
"value": ["{{REF:links.docsEmbed}}", "{{REF:links.supportEmbed}}"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"helpContactSupport": {
|
||||
"title": "Help - {{REF:helpOptions.contactSupport}}",
|
||||
"description": [
|
||||
"Have a question or feedback? Join our support server at the link below!"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "{{REF:fields.links}}",
|
||||
"value": ["{{REF:links.supportEmbed}}"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"helpCommands": {
|
||||
"title": "Help - {{REF:helpOptions.commands}}",
|
||||
"fields": [
|
||||
{
|
||||
"name": "Commands",
|
||||
"value": [
|
||||
"To see the available commands, just type `/` and select the bot from the left side. You can then scroll through all available commands. Some commands may be hidden if you don't have permission to view them.",
|
||||
"",
|
||||
"{{CMD_LINK_TEST}} - {{REF:commandDescs.test}}",
|
||||
"{{CMD_LINK_INFO}} - {{REF:commandDescs.info}}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Command Permissions",
|
||||
"value": [
|
||||
"Want to restrict commands to certain roles, users, or channels? Set up permissions in the bot's integration page by going to **Server Settings** > **Integrations**, and then **Manage** for this bot."
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "{{REF:fields.links}}",
|
||||
"value": ["{{REF:links.docsEmbed}}", "{{REF:links.supportEmbed}}"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"test": {
|
||||
"description": "Test command works!"
|
||||
},
|
||||
"viewDateJoined": {
|
||||
"description": "{{TARGET}} joined on {{DATE}}!"
|
||||
},
|
||||
"viewDateSent": {
|
||||
"description": "This message was sent on {{DATE}}!"
|
||||
},
|
||||
"about": {
|
||||
"title": "{{COM:bot.name}} - About",
|
||||
"description": "{{REF:bot.description}}",
|
||||
"fields": [
|
||||
{ "name": "Author", "value": "{{REF:links.authorEmbed}}" },
|
||||
{
|
||||
"name": "{{REF:fields.links}}",
|
||||
"value": [
|
||||
"{{REF:links.sourceEmbed}}",
|
||||
"{{REF:links.docsEmbed}}",
|
||||
"{{REF:links.termsEmbed}}",
|
||||
"{{REF:links.voteEmbed}}",
|
||||
"{{REF:links.donateEmbed}}",
|
||||
"{{REF:links.supportEmbed}}",
|
||||
"{{REF:links.inviteEmbed}}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Created With",
|
||||
"value": ["{{REF:links.templateEmbed}}"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"translate": {
|
||||
"title": "{{COM:bot.name}} - Translations",
|
||||
"description": "Thank you to our translators who have made it possible for {{COM:bot.name}} to be used in the following languages. If you are interested in providing a translation, please contact the staff in our [support server]({{COM:links.support}})."
|
||||
},
|
||||
"devInfo": {
|
||||
"title": "{{COM:bot.name}} - Developer Info",
|
||||
"fields": [
|
||||
{
|
||||
"name": "Versions",
|
||||
"value": [
|
||||
"**Node.js**: {{NODE_VERSION}}",
|
||||
"**TypeScript**: {{TS_VERSION}}",
|
||||
"**ECMAScript**: {{ES_VERSION}}",
|
||||
"**discord.js**: {{DJS_VERSION}}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Stats",
|
||||
"value": [
|
||||
"**Shards**: {{SHARD_COUNT}}",
|
||||
"**Servers**: {{SERVER_COUNT}} ({{SERVER_COUNT_PER_SHARD}}/Shard)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Memory",
|
||||
"value": [
|
||||
"**RSS**: {{RSS_SIZE}} ({{RSS_SIZE_PER_SERVER}}/Server)",
|
||||
"**Heap**: {{HEAP_TOTAL_SIZE}} ({{HEAP_TOTAL_SIZE_PER_SERVER}}/Server)",
|
||||
"**Used**: {{HEAP_USED_SIZE}} ({{HEAP_USED_SIZE_PER_SERVER}}/Server)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "IDs",
|
||||
"value": [
|
||||
"**Hostname**: {{HOSTNAME}}",
|
||||
"**Shard ID**: {{SHARD_ID}}",
|
||||
"**Server ID**: {{SERVER_ID}}",
|
||||
"**Bot ID**: {{BOT_ID}}",
|
||||
"**User ID**: {{USER_ID}}"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"validationEmbeds": {
|
||||
"cooldownHit": {
|
||||
"description": "You can only run this command {{AMOUNT}} time(s) every {{INTERVAL}}. Please wait before attempting this command again.",
|
||||
"color": "{{COM:colors.warning}}"
|
||||
},
|
||||
"devOnly": {
|
||||
"description": "This action can only be done by developers.",
|
||||
"color": "{{COM:colors.warning}}"
|
||||
},
|
||||
"missingClientPerms": {
|
||||
"description": [
|
||||
"I don't have all permissions required to run that command here! Please check the server and channel permissions to make sure I have the following permissions.",
|
||||
"",
|
||||
"Required permissions: {{PERMISSIONS}}"
|
||||
],
|
||||
"color": "{{COM:colors.warning}}"
|
||||
}
|
||||
},
|
||||
"errorEmbeds": {
|
||||
"command": {
|
||||
"description": "Something went wrong!",
|
||||
"fields": [
|
||||
{
|
||||
"name": "Error code",
|
||||
"value": "{{ERROR_CODE}}"
|
||||
},
|
||||
{
|
||||
"name": "Server ID",
|
||||
"value": "{{GUILD_ID}}"
|
||||
},
|
||||
{
|
||||
"name": "Shard ID",
|
||||
"value": "{{SHARD_ID}}"
|
||||
},
|
||||
{
|
||||
"name": "Contact support",
|
||||
"value": "{{COM:links.support}}"
|
||||
}
|
||||
],
|
||||
"color": "{{COM:colors.error}}"
|
||||
},
|
||||
"startupInProcess": {
|
||||
"description": "{{COM:bot.name}} is still starting up. Try again later.",
|
||||
"color": "{{COM:colors.warning}}"
|
||||
},
|
||||
"notImplemented": {
|
||||
"description": "This feature has not been implemented yet!",
|
||||
"color": "{{COM:colors.warning}}"
|
||||
}
|
||||
},
|
||||
"channelRegexes": {
|
||||
"bot": "/bot|command|cmd/i"
|
||||
}
|
||||
},
|
||||
"refs": {
|
||||
"meta": {
|
||||
"translators": "[TranslatorName#1234](https://github.com/)"
|
||||
},
|
||||
"bot": {
|
||||
"description": "{{REF:links.templateEmbed}} helps give developers a starting point for new Discord bots, so that much of the initial setup can be avoided and developers can instead focus on meaningful bot features."
|
||||
},
|
||||
"chatCommands": {
|
||||
"dev": "dev",
|
||||
"help": "help",
|
||||
"info": "info",
|
||||
"test": "test"
|
||||
},
|
||||
"userCommands": {
|
||||
"viewDateJoined": "View Date Joined"
|
||||
},
|
||||
"messageCommands": {
|
||||
"viewDateSent": "View Date Sent"
|
||||
},
|
||||
"arguments": {
|
||||
"command": "command",
|
||||
"option": "option"
|
||||
},
|
||||
"commandDescs": {
|
||||
"dev": "Developer use only.",
|
||||
"help": "Find help or contact support.",
|
||||
"info": "View bot info.",
|
||||
"test": "Run the test command."
|
||||
},
|
||||
"argDescs": {
|
||||
"devCommand": "Command.",
|
||||
"helpOption": "Option.",
|
||||
"infoOption": "Option."
|
||||
},
|
||||
"fields": {
|
||||
"commands": "Commands",
|
||||
"links": "Links"
|
||||
},
|
||||
"permissions": {
|
||||
"AddReactions": "Add Reactions",
|
||||
"Administrator": "Administrator",
|
||||
"AttachFiles": "Attach Files",
|
||||
"BanMembers": "Ban Members",
|
||||
"ChangeNickname": "Change Nickname",
|
||||
"Connect": "Connect",
|
||||
"CreateInstantInvite": "Create Invite",
|
||||
"CreatePrivateThreads": "Create Private Threads",
|
||||
"CreatePublicThreads": "Create Public Threads",
|
||||
"DeafenMembers": "Deafen Members",
|
||||
"EmbedLinks": "Embed Links",
|
||||
"KickMembers": "Kick Members",
|
||||
"ManageChannels": "Manage Channel(s)",
|
||||
"ManageEmojisAndStickers": "Manage Emoji and Stickers",
|
||||
"ManageEvents": "Manage Events",
|
||||
"ManageGuild": "Manage Server",
|
||||
"ManageGuildExpressions": "Manage Expressions",
|
||||
"ManageMessages": "Manage Messages",
|
||||
"ManageNicknames": "Manage Nicknames",
|
||||
"ManageRoles": "Manage Roles / Permissions",
|
||||
"ManageThreads": "Manage Threads / Posts",
|
||||
"ManageWebhooks": "Manage Webhooks",
|
||||
"MentionEveryone": "Mention Everyone, Here, and All Roles",
|
||||
"ModerateMembers": "Timeout Members",
|
||||
"MoveMembers": "Move Members",
|
||||
"MuteMembers": "Mute Members",
|
||||
"PrioritySpeaker": "Priority Speaker",
|
||||
"ReadMessageHistory": "Read Message History",
|
||||
"RequestToSpeak": "Request to Speak",
|
||||
"SendMessages": "Send Messages / Create Posts",
|
||||
"SendMessagesInThreads": "Send Messages in Threads / Posts",
|
||||
"SendTTSMessages": "Send Text-to-Speech Messages",
|
||||
"SendVoiceMessages": "Send Voice Messages",
|
||||
"Speak": "Speak",
|
||||
"Stream": "Video",
|
||||
"UseApplicationCommands": "Use Application Commands",
|
||||
"UseEmbeddedActivities": "Use Activities",
|
||||
"UseExternalEmojis": "Use External Emoji",
|
||||
"UseExternalSounds": "Use External Sounds",
|
||||
"UseExternalStickers": "Use External Stickers",
|
||||
"UseSoundboard": "Use Soundboard",
|
||||
"UseVAD": "Use Voice Activity",
|
||||
"ViewAuditLog": "View Audit Log",
|
||||
"ViewChannel": "View Channel(s)",
|
||||
"ViewCreatorMonetizationAnalytics": "View Server Subscription Insights",
|
||||
"ViewGuildInsights": "View Server Insights"
|
||||
},
|
||||
"devCommandNames": {
|
||||
"info": "info"
|
||||
},
|
||||
"helpOptions": {
|
||||
"contactSupport": "Contact Support",
|
||||
"commands": "Commands"
|
||||
},
|
||||
"helpOptionDescs": {
|
||||
"contactSupport": "❓ {{REF:helpOptions.contactSupport}} ❓",
|
||||
"commands": "{{REF:helpOptions.commands}} -- What commands are there? How do I restrict who is allowed to use commands?"
|
||||
},
|
||||
"infoOptions": {
|
||||
"about": "About",
|
||||
"translate": "Translate"
|
||||
},
|
||||
"yesNo": {
|
||||
"yes": "Yes",
|
||||
"no": "No"
|
||||
},
|
||||
"boolean": {
|
||||
"true": "True",
|
||||
"false": "False"
|
||||
},
|
||||
"other": {
|
||||
"na": "N/A"
|
||||
},
|
||||
"links": {
|
||||
"authorEmbed": "[{{COM:bot.author}}]({{COM:links.author}})",
|
||||
"docsEmbed": "[View Documentation]({{COM:links.docs}})",
|
||||
"donateEmbed": "[Donate via PayPal]({{COM:links.donate}})",
|
||||
"inviteEmbed": "[Invite {{COM:bot.name}} to a Server!]({{COM:links.invite}})",
|
||||
"sourceEmbed": "[View Source Code]({{COM:links.source}})",
|
||||
"supportEmbed": "[Join Support Server]({{COM:links.support}})",
|
||||
"templateEmbed": "[Discord Bot TypeScript Template]({{COM:links.template}})",
|
||||
"termsEmbed": "[View Terms of Service]({{COM:links.terms}})",
|
||||
"voteEmbed": "[Vote for {{COM:bot.name}}!]({{COM:links.vote}})"
|
||||
}
|
||||
}
|
||||
}
|
||||
60
lang/logs.json
Normal file
60
lang/logs.json
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"info": {
|
||||
"appStarted": "Application started.",
|
||||
"apiStarted": "API started on port {PORT}.",
|
||||
"commandActionView": "\nLocal and remote:\n {LOCAL_AND_REMOTE_LIST}\nLocal only:\n {LOCAL_ONLY_LIST}\nRemote only:\n {REMOTE_ONLY_LIST}",
|
||||
"commandActionCreating": "Creating commands: {COMMAND_LIST}",
|
||||
"commandActionCreated": "Commands created.",
|
||||
"commandActionUpdating": "Updating commands: {COMMAND_LIST}",
|
||||
"commandActionUpdated": "Commands updated.",
|
||||
"commandActionRenaming": "Renaming command: '{OLD_COMMAND_NAME}' --> '{NEW_COMMAND_NAME}'",
|
||||
"commandActionRenamed": "Command renamed.",
|
||||
"commandActionDeleting": "Deleting command: '{COMMAND_NAME}'",
|
||||
"commandActionDeleted": "Command deleted.",
|
||||
"commandActionClearing": "Deleting all commands: {COMMAND_LIST}",
|
||||
"commandActionCleared": "Commands deleted.",
|
||||
"managerSpawningShards": "Spawning {SHARD_COUNT} shards: [{SHARD_LIST}].",
|
||||
"managerLaunchedShard": "Launched Shard {SHARD_ID}.",
|
||||
"managerAllShardsSpawned": "All shards have been spawned.",
|
||||
"clientLogin": "Client logged in as '{USER_TAG}'.",
|
||||
"clientReady": "Client is ready!",
|
||||
"jobScheduled": "Scheduled job '{JOB}' for '{SCHEDULE}'.",
|
||||
"jobRun": "Running job '{JOB}'.",
|
||||
"jobCompleted": "Job '{JOB}' completed.",
|
||||
"updatedServerCount": "Updated server count. Connected to {SERVER_COUNT} total servers.",
|
||||
"updatedServerCountSite": "Updated server count on '{BOT_SITE}'.",
|
||||
"guildJoined": "Guild '{GUILD_NAME}' ({GUILD_ID}) joined.",
|
||||
"guildLeft": "Guild '{GUILD_NAME}' ({GUILD_ID}) left."
|
||||
},
|
||||
"warn": {
|
||||
"managerNoShards": "No shards to spawn."
|
||||
},
|
||||
"error": {
|
||||
"unspecified": "An unspecified error occurred.",
|
||||
"unhandledRejection": "An unhandled promise rejection occurred.",
|
||||
"retrieveShards": "An error occurred while retrieving which shards to spawn.",
|
||||
"managerSpawningShards": "An error occurred while spawning shards.",
|
||||
"managerShardInfo": "An error occurred while retrieving shard info.",
|
||||
"commandAction": "An error occurred while running a command action.",
|
||||
"commandActionNotFound": "Could not find a command with the name '{COMMAND_NAME}'.",
|
||||
"commandActionRenameMissingArg": "Please supply the current command name and new command name.",
|
||||
"commandActionDeleteMissingArg": "Please supply a command name to delete.",
|
||||
"clientLogin": "An error occurred while the client attempted to login.",
|
||||
"job": "An error occurred while running the '{JOB}' job.",
|
||||
"updatedServerCountSite": "An error occurred while updating the server count on '{BOT_SITE}'.",
|
||||
"guildJoin": "An error occurred while processing a guild join.",
|
||||
"guildLeave": "An error occurred while processing a guild leave.",
|
||||
"message": "An error occurred while processing a message.",
|
||||
"reaction": "An error occurred while processing a reaction.",
|
||||
"command": "An error occurred while processing a command interaction.",
|
||||
"button": "An error occurred while processing a button interaction.",
|
||||
"commandNotFound": "[{INTERACTION_ID}] A command with the name '{COMMAND_NAME}' could not be found.",
|
||||
"autocompleteNotFound": "[{INTERACTION_ID}] An autocomplete method for the '{COMMAND_NAME}' command could not be found.",
|
||||
"commandGuild": "[{INTERACTION_ID}] An error occurred while executing the '{COMMAND_NAME}' command for user '{USER_TAG}' ({USER_ID}) in channel '{CHANNEL_NAME}' ({CHANNEL_ID}) in guild '{GUILD_NAME}' ({GUILD_ID}).",
|
||||
"autocompleteGuild": "[{INTERACTION_ID}] An error occurred while autocompleting the '{OPTION_NAME}' option for the '{COMMAND_NAME}' command for user '{USER_TAG}' ({USER_ID}) in channel '{CHANNEL_NAME}' ({CHANNEL_ID}) in guild '{GUILD_NAME}' ({GUILD_ID}).",
|
||||
"commandOther": "[{INTERACTION_ID}] An error occurred while executing the '{COMMAND_NAME}' command for user '{USER_TAG}' ({USER_ID}).",
|
||||
"autocompleteOther": "[{INTERACTION_ID}] An error occurred while autocompleting the '{OPTION_NAME}' option for the '{COMMAND_NAME}' command for user '{USER_TAG}' ({USER_ID}).",
|
||||
"apiRequest": "An error occurred while processing a '{HTTP_METHOD}' request to '{URL}'.",
|
||||
"apiRateLimit": "A rate limit was hit while making a request."
|
||||
}
|
||||
}
|
||||
135
misc/Discord Bot Cluster API.postman_collection.json
Normal file
135
misc/Discord Bot Cluster API.postman_collection.json
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
{
|
||||
"info": {
|
||||
"_postman_id": "d37e9bae-0a24-4940-af63-2716ab3bb660",
|
||||
"name": "Discord Bot Cluster API",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "Shards",
|
||||
"item": [
|
||||
{
|
||||
"name": "Get Shards",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{BASE_URL}}/shards",
|
||||
"host": [
|
||||
"{{BASE_URL}}"
|
||||
],
|
||||
"path": [
|
||||
"shards"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Set Shard Presences",
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\r\n \"type\": \"STREAMING\",\r\n \"name\": \"to 1,000,000 servers\",\r\n \"url\": \"https://www.twitch.tv/novakevin\"\r\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{BASE_URL}}/shards/presence",
|
||||
"host": [
|
||||
"{{BASE_URL}}"
|
||||
],
|
||||
"path": [
|
||||
"shards",
|
||||
"presence"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Guilds",
|
||||
"item": [
|
||||
{
|
||||
"name": "Get Guilds",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{BASE_URL}}/guilds",
|
||||
"host": [
|
||||
"{{BASE_URL}}"
|
||||
],
|
||||
"path": [
|
||||
"guilds"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Get Root",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{BASE_URL}}",
|
||||
"host": [
|
||||
"{{BASE_URL}}"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"type": "apikey",
|
||||
"apikey": [
|
||||
{
|
||||
"key": "key",
|
||||
"value": "Authorization",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "value",
|
||||
"value": "00000000-0000-0000-0000-000000000000",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "prerequest",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
""
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
""
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"variable": [
|
||||
{
|
||||
"key": "BASE_URL",
|
||||
"value": "localhost:3001"
|
||||
}
|
||||
]
|
||||
}
|
||||
5989
package-lock.json
generated
Normal file
5989
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
71
package.json
Normal file
71
package.json
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
{
|
||||
"name": "my-bot",
|
||||
"version": "1.0.0",
|
||||
"author": "Kevin Novak",
|
||||
"description": "A discord.js bot template written with TypeScript",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
},
|
||||
"type": "module",
|
||||
"exports": [
|
||||
"./dist/start-bot.js",
|
||||
"./dist/start-manager.js"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "eslint . --cache --ext .js,.jsx,.ts,.tsx",
|
||||
"lint:fix": "eslint . --fix --cache --ext .js,.jsx,.ts,.tsx",
|
||||
"format": "prettier --check .",
|
||||
"format:fix": "prettier --write .",
|
||||
"clean": "git clean -xdf --exclude=\"/config/**/*\"",
|
||||
"clean:dry": "git clean -xdf --exclude=\"/config/**/*\" --dry-run",
|
||||
"build": "tsc --project tsconfig.json",
|
||||
"commands:view": "npm run build && node --enable-source-maps dist/start-bot.js commands view",
|
||||
"commands:register": "npm run build && node --enable-source-maps dist/start-bot.js commands register",
|
||||
"commands:rename": "npm run build && node --enable-source-maps dist/start-bot.js commands rename",
|
||||
"commands:delete": "npm run build && node --enable-source-maps dist/start-bot.js commands delete",
|
||||
"commands:clear": "npm run build && node --enable-source-maps dist/start-bot.js commands clear",
|
||||
"start": "npm run start:bot",
|
||||
"start:bot": "npm run build && node --enable-source-maps dist/start-bot.js",
|
||||
"start:manager": "npm run build && node --enable-source-maps dist/start-manager.js",
|
||||
"start:pm2": "npm run build && npm run pm2:start",
|
||||
"pm2:start": "pm2 start process.json",
|
||||
"pm2:stop": "pm2 stop process.json",
|
||||
"pm2:delete": "pm2 delete process.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordjs/rest": "2.0.1",
|
||||
"class-transformer": "0.5.1",
|
||||
"class-validator": "0.14.0",
|
||||
"cron-parser": "^4.9.0",
|
||||
"discord.js": "14.13.0",
|
||||
"discord.js-rate-limiter": "1.3.2",
|
||||
"express": "4.18.2",
|
||||
"express-promise-router": "4.1.1",
|
||||
"filesize": "10.0.12",
|
||||
"linguini": "1.3.1",
|
||||
"luxon": "3.4.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"node-schedule": "2.1.1",
|
||||
"pino": "8.15.0",
|
||||
"pino-pretty": "10.2.0",
|
||||
"pm2": "^5.3.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"remove-markdown": "0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "4.17.17",
|
||||
"@types/luxon": "3.3.1",
|
||||
"@types/node": "^20.5.0",
|
||||
"@types/node-schedule": "2.1.0",
|
||||
"@types/remove-markdown": "0.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.4.0",
|
||||
"@typescript-eslint/parser": "^6.4.0",
|
||||
"eslint": "^8.47.0",
|
||||
"eslint-plugin-import": "^2.28.0",
|
||||
"eslint-plugin-unicorn": "^48.0.1",
|
||||
"prettier": "^3.0.2",
|
||||
"typescript": "^5.1.6"
|
||||
}
|
||||
}
|
||||
10
process.json
Normal file
10
process.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"apps": [
|
||||
{
|
||||
"name": "my-bot",
|
||||
"script": "dist/start-manager.js",
|
||||
"node_args": ["--enable-source-maps"],
|
||||
"restart_delay": 10000
|
||||
}
|
||||
]
|
||||
}
|
||||
17
src/buttons/button.ts
Normal file
17
src/buttons/button.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { ButtonInteraction } from 'discord.js';
|
||||
|
||||
import { EventData } from '../models/internal-models.js';
|
||||
|
||||
export interface Button {
|
||||
ids: string[];
|
||||
deferType: ButtonDeferType;
|
||||
requireGuild: boolean;
|
||||
requireEmbedAuthorTag: boolean;
|
||||
execute(intr: ButtonInteraction, data: EventData): Promise<void>;
|
||||
}
|
||||
|
||||
export enum ButtonDeferType {
|
||||
REPLY = 'REPLY',
|
||||
UPDATE = 'UPDATE',
|
||||
NONE = 'NONE',
|
||||
}
|
||||
1
src/buttons/index.ts
Normal file
1
src/buttons/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { Button, ButtonDeferType } from './button.js';
|
||||
60
src/commands/args.ts
Normal file
60
src/commands/args.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { APIApplicationCommandBasicOption, ApplicationCommandOptionType } from 'discord.js';
|
||||
|
||||
import { DevCommandName, HelpOption, InfoOption } from '../enums/index.js';
|
||||
import { Language } from '../models/enum-helpers/index.js';
|
||||
import { Lang } from '../services/index.js';
|
||||
|
||||
export class Args {
|
||||
public static readonly DEV_COMMAND: APIApplicationCommandBasicOption = {
|
||||
name: Lang.getRef('arguments.command', Language.Default),
|
||||
name_localizations: Lang.getRefLocalizationMap('arguments.command'),
|
||||
description: Lang.getRef('argDescs.devCommand', Language.Default),
|
||||
description_localizations: Lang.getRefLocalizationMap('argDescs.devCommand'),
|
||||
type: ApplicationCommandOptionType.String,
|
||||
choices: [
|
||||
{
|
||||
name: Lang.getRef('devCommandNames.info', Language.Default),
|
||||
name_localizations: Lang.getRefLocalizationMap('devCommandNames.info'),
|
||||
value: DevCommandName.INFO,
|
||||
},
|
||||
],
|
||||
};
|
||||
public static readonly HELP_OPTION: APIApplicationCommandBasicOption = {
|
||||
name: Lang.getRef('arguments.option', Language.Default),
|
||||
name_localizations: Lang.getRefLocalizationMap('arguments.option'),
|
||||
description: Lang.getRef('argDescs.helpOption', Language.Default),
|
||||
description_localizations: Lang.getRefLocalizationMap('argDescs.helpOption'),
|
||||
type: ApplicationCommandOptionType.String,
|
||||
choices: [
|
||||
{
|
||||
name: Lang.getRef('helpOptionDescs.contactSupport', Language.Default),
|
||||
name_localizations: Lang.getRefLocalizationMap('helpOptionDescs.contactSupport'),
|
||||
value: HelpOption.CONTACT_SUPPORT,
|
||||
},
|
||||
{
|
||||
name: Lang.getRef('helpOptionDescs.commands', Language.Default),
|
||||
name_localizations: Lang.getRefLocalizationMap('helpOptionDescs.commands'),
|
||||
value: HelpOption.COMMANDS,
|
||||
},
|
||||
],
|
||||
};
|
||||
public static readonly INFO_OPTION: APIApplicationCommandBasicOption = {
|
||||
name: Lang.getRef('arguments.option', Language.Default),
|
||||
name_localizations: Lang.getRefLocalizationMap('arguments.option'),
|
||||
description: Lang.getRef('argDescs.helpOption', Language.Default),
|
||||
description_localizations: Lang.getRefLocalizationMap('argDescs.helpOption'),
|
||||
type: ApplicationCommandOptionType.String,
|
||||
choices: [
|
||||
{
|
||||
name: Lang.getRef('infoOptions.about', Language.Default),
|
||||
name_localizations: Lang.getRefLocalizationMap('infoOptions.about'),
|
||||
value: InfoOption.ABOUT,
|
||||
},
|
||||
{
|
||||
name: Lang.getRef('infoOptions.translate', Language.Default),
|
||||
name_localizations: Lang.getRefLocalizationMap('infoOptions.translate'),
|
||||
value: InfoOption.TRANSLATE,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
98
src/commands/chat/dev-command.ts
Normal file
98
src/commands/chat/dev-command.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import djs, { ChatInputCommandInteraction, PermissionsString } from 'discord.js';
|
||||
import { createRequire } from 'node:module';
|
||||
import os from 'node:os';
|
||||
import typescript from 'typescript';
|
||||
|
||||
import { DevCommandName } from '../../enums/index.js';
|
||||
import { Language } from '../../models/enum-helpers/index.js';
|
||||
import { EventData } from '../../models/internal-models.js';
|
||||
import { Lang } from '../../services/index.js';
|
||||
import { FormatUtils, InteractionUtils, ShardUtils } from '../../utils/index.js';
|
||||
import { Command, CommandDeferType } from '../index.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
let Config = require('../../../config/config.json');
|
||||
let TsConfig = require('../../../tsconfig.json');
|
||||
|
||||
export class DevCommand implements Command {
|
||||
public names = [Lang.getRef('chatCommands.dev', Language.Default)];
|
||||
public deferType = CommandDeferType.HIDDEN;
|
||||
public requireClientPerms: PermissionsString[] = [];
|
||||
public async execute(intr: ChatInputCommandInteraction, data: EventData): Promise<void> {
|
||||
if (!Config.developers.includes(intr.user.id)) {
|
||||
await InteractionUtils.send(intr, Lang.getEmbed('validationEmbeds.devOnly', data.lang));
|
||||
return;
|
||||
}
|
||||
|
||||
let args = {
|
||||
command: intr.options.getString(
|
||||
Lang.getRef('arguments.command', Language.Default)
|
||||
) as DevCommandName,
|
||||
};
|
||||
|
||||
switch (args.command) {
|
||||
case DevCommandName.INFO: {
|
||||
let shardCount = intr.client.shard?.count ?? 1;
|
||||
let serverCount: number;
|
||||
if (intr.client.shard) {
|
||||
try {
|
||||
serverCount = await ShardUtils.serverCount(intr.client.shard);
|
||||
} catch (error) {
|
||||
if (error.name.includes('ShardingInProcess')) {
|
||||
await InteractionUtils.send(
|
||||
intr,
|
||||
Lang.getEmbed('errorEmbeds.startupInProcess', data.lang)
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
serverCount = intr.client.guilds.cache.size;
|
||||
}
|
||||
|
||||
let memory = process.memoryUsage();
|
||||
|
||||
await InteractionUtils.send(
|
||||
intr,
|
||||
Lang.getEmbed('displayEmbeds.devInfo', data.lang, {
|
||||
NODE_VERSION: process.version,
|
||||
TS_VERSION: `v${typescript.version}`,
|
||||
ES_VERSION: TsConfig.compilerOptions.target,
|
||||
DJS_VERSION: `v${djs.version}`,
|
||||
SHARD_COUNT: shardCount.toLocaleString(data.lang),
|
||||
SERVER_COUNT: serverCount.toLocaleString(data.lang),
|
||||
SERVER_COUNT_PER_SHARD: Math.round(serverCount / shardCount).toLocaleString(
|
||||
data.lang
|
||||
),
|
||||
RSS_SIZE: FormatUtils.fileSize(memory.rss),
|
||||
RSS_SIZE_PER_SERVER:
|
||||
serverCount > 0
|
||||
? FormatUtils.fileSize(memory.rss / serverCount)
|
||||
: Lang.getRef('other.na', data.lang),
|
||||
HEAP_TOTAL_SIZE: FormatUtils.fileSize(memory.heapTotal),
|
||||
HEAP_TOTAL_SIZE_PER_SERVER:
|
||||
serverCount > 0
|
||||
? FormatUtils.fileSize(memory.heapTotal / serverCount)
|
||||
: Lang.getRef('other.na', data.lang),
|
||||
HEAP_USED_SIZE: FormatUtils.fileSize(memory.heapUsed),
|
||||
HEAP_USED_SIZE_PER_SERVER:
|
||||
serverCount > 0
|
||||
? FormatUtils.fileSize(memory.heapUsed / serverCount)
|
||||
: Lang.getRef('other.na', data.lang),
|
||||
HOSTNAME: os.hostname(),
|
||||
SHARD_ID: (intr.guild?.shardId ?? 0).toString(),
|
||||
SERVER_ID: intr.guild?.id ?? Lang.getRef('other.na', data.lang),
|
||||
BOT_ID: intr.client.user?.id,
|
||||
USER_ID: intr.user.id,
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/commands/chat/help-command.ts
Normal file
51
src/commands/chat/help-command.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { ChatInputCommandInteraction, EmbedBuilder, PermissionsString } from 'discord.js';
|
||||
|
||||
import { HelpOption } from '../../enums/index.js';
|
||||
import { Language } from '../../models/enum-helpers/index.js';
|
||||
import { EventData } from '../../models/internal-models.js';
|
||||
import { Lang } from '../../services/index.js';
|
||||
import { ClientUtils, FormatUtils, InteractionUtils } from '../../utils/index.js';
|
||||
import { Command, CommandDeferType } from '../index.js';
|
||||
|
||||
export class HelpCommand implements Command {
|
||||
public names = [Lang.getRef('chatCommands.help', Language.Default)];
|
||||
public deferType = CommandDeferType.HIDDEN;
|
||||
public requireClientPerms: PermissionsString[] = [];
|
||||
public async execute(intr: ChatInputCommandInteraction, data: EventData): Promise<void> {
|
||||
let args = {
|
||||
option: intr.options.getString(
|
||||
Lang.getRef('arguments.option', Language.Default)
|
||||
) as HelpOption,
|
||||
};
|
||||
|
||||
let embed: EmbedBuilder;
|
||||
switch (args.option) {
|
||||
case HelpOption.CONTACT_SUPPORT: {
|
||||
embed = Lang.getEmbed('displayEmbeds.helpContactSupport', data.lang);
|
||||
break;
|
||||
}
|
||||
case HelpOption.COMMANDS: {
|
||||
embed = Lang.getEmbed('displayEmbeds.helpCommands', data.lang, {
|
||||
CMD_LINK_TEST: FormatUtils.commandMention(
|
||||
await ClientUtils.findAppCommand(
|
||||
intr.client,
|
||||
Lang.getRef('chatCommands.test', Language.Default)
|
||||
)
|
||||
),
|
||||
CMD_LINK_INFO: FormatUtils.commandMention(
|
||||
await ClientUtils.findAppCommand(
|
||||
intr.client,
|
||||
Lang.getRef('chatCommands.info', Language.Default)
|
||||
)
|
||||
),
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await InteractionUtils.send(intr, embed);
|
||||
}
|
||||
}
|
||||
4
src/commands/chat/index.ts
Normal file
4
src/commands/chat/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export { DevCommand } from './dev-command.js';
|
||||
export { HelpCommand } from './help-command.js';
|
||||
export { InfoCommand } from './info-command.js';
|
||||
export { TestCommand } from './test-command.js';
|
||||
47
src/commands/chat/info-command.ts
Normal file
47
src/commands/chat/info-command.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { ChatInputCommandInteraction, EmbedBuilder, PermissionsString } from 'discord.js';
|
||||
|
||||
import { InfoOption } from '../../enums/index.js';
|
||||
import { Language } from '../../models/enum-helpers/index.js';
|
||||
import { EventData } from '../../models/internal-models.js';
|
||||
import { Lang } from '../../services/index.js';
|
||||
import { InteractionUtils } from '../../utils/index.js';
|
||||
import { Command, CommandDeferType } from '../index.js';
|
||||
|
||||
export class InfoCommand implements Command {
|
||||
public names = [Lang.getRef('chatCommands.info', Language.Default)];
|
||||
public deferType = CommandDeferType.HIDDEN;
|
||||
public requireClientPerms: PermissionsString[] = [];
|
||||
|
||||
public async execute(intr: ChatInputCommandInteraction, data: EventData): Promise<void> {
|
||||
let args = {
|
||||
option: intr.options.getString(
|
||||
Lang.getRef('arguments.option', Language.Default)
|
||||
) as InfoOption,
|
||||
};
|
||||
|
||||
let embed: EmbedBuilder;
|
||||
switch (args.option) {
|
||||
case InfoOption.ABOUT: {
|
||||
embed = Lang.getEmbed('displayEmbeds.about', data.lang);
|
||||
break;
|
||||
}
|
||||
case InfoOption.TRANSLATE: {
|
||||
embed = Lang.getEmbed('displayEmbeds.translate', data.lang);
|
||||
for (let langCode of Language.Enabled) {
|
||||
embed.addFields([
|
||||
{
|
||||
name: Language.Data[langCode].nativeName,
|
||||
value: Lang.getRef('meta.translators', langCode),
|
||||
},
|
||||
]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await InteractionUtils.send(intr, embed);
|
||||
}
|
||||
}
|
||||
19
src/commands/chat/test-command.ts
Normal file
19
src/commands/chat/test-command.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { ChatInputCommandInteraction, PermissionsString } from 'discord.js';
|
||||
import { RateLimiter } from 'discord.js-rate-limiter';
|
||||
|
||||
import { Language } from '../../models/enum-helpers/index.js';
|
||||
import { EventData } from '../../models/internal-models.js';
|
||||
import { Lang } from '../../services/index.js';
|
||||
import { InteractionUtils } from '../../utils/index.js';
|
||||
import { Command, CommandDeferType } from '../index.js';
|
||||
|
||||
export class TestCommand implements Command {
|
||||
public names = [Lang.getRef('chatCommands.test', Language.Default)];
|
||||
public cooldown = new RateLimiter(1, 5000);
|
||||
public deferType = CommandDeferType.HIDDEN;
|
||||
public requireClientPerms: PermissionsString[] = [];
|
||||
|
||||
public async execute(intr: ChatInputCommandInteraction, data: EventData): Promise<void> {
|
||||
await InteractionUtils.send(intr, Lang.getEmbed('displayEmbeds.test', data.lang));
|
||||
}
|
||||
}
|
||||
28
src/commands/command.ts
Normal file
28
src/commands/command.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import {
|
||||
ApplicationCommandOptionChoiceData,
|
||||
AutocompleteFocusedOption,
|
||||
AutocompleteInteraction,
|
||||
CommandInteraction,
|
||||
PermissionsString,
|
||||
} from 'discord.js';
|
||||
import { RateLimiter } from 'discord.js-rate-limiter';
|
||||
|
||||
import { EventData } from '../models/internal-models.js';
|
||||
|
||||
export interface Command {
|
||||
names: string[];
|
||||
cooldown?: RateLimiter;
|
||||
deferType: CommandDeferType;
|
||||
requireClientPerms: PermissionsString[];
|
||||
autocomplete?(
|
||||
intr: AutocompleteInteraction,
|
||||
option: AutocompleteFocusedOption
|
||||
): Promise<ApplicationCommandOptionChoiceData[]>;
|
||||
execute(intr: CommandInteraction, data: EventData): Promise<void>;
|
||||
}
|
||||
|
||||
export enum CommandDeferType {
|
||||
PUBLIC = 'PUBLIC',
|
||||
HIDDEN = 'HIDDEN',
|
||||
NONE = 'NONE',
|
||||
}
|
||||
3
src/commands/index.ts
Normal file
3
src/commands/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { Args } from './args.js';
|
||||
export { Command, CommandDeferType } from './command.js';
|
||||
export { ChatCommandMetadata, MessageCommandMetadata, UserCommandMetadata } from './metadata.js';
|
||||
1
src/commands/message/index.ts
Normal file
1
src/commands/message/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { ViewDateSent } from './view-date-sent.js';
|
||||
30
src/commands/message/view-date-sent.ts
Normal file
30
src/commands/message/view-date-sent.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { MessageContextMenuCommandInteraction, PermissionsString } from 'discord.js';
|
||||
import { RateLimiter } from 'discord.js-rate-limiter';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { Language } from '../../models/enum-helpers/index.js';
|
||||
import { EventData } from '../../models/internal-models.js';
|
||||
import { Lang } from '../../services/index.js';
|
||||
import { InteractionUtils } from '../../utils/index.js';
|
||||
import { Command, CommandDeferType } from '../index.js';
|
||||
|
||||
export class ViewDateSent implements Command {
|
||||
public names = [Lang.getRef('messageCommands.viewDateSent', Language.Default)];
|
||||
public cooldown = new RateLimiter(1, 5000);
|
||||
public deferType = CommandDeferType.HIDDEN;
|
||||
public requireClientPerms: PermissionsString[] = [];
|
||||
|
||||
public async execute(
|
||||
intr: MessageContextMenuCommandInteraction,
|
||||
data: EventData
|
||||
): Promise<void> {
|
||||
await InteractionUtils.send(
|
||||
intr,
|
||||
Lang.getEmbed('displayEmbeds.viewDateSent', data.lang, {
|
||||
DATE: DateTime.fromJSDate(intr.targetMessage.createdAt).toLocaleString(
|
||||
DateTime.DATE_HUGE
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
96
src/commands/metadata.ts
Normal file
96
src/commands/metadata.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import {
|
||||
ApplicationCommandType,
|
||||
PermissionFlagsBits,
|
||||
PermissionsBitField,
|
||||
RESTPostAPIChatInputApplicationCommandsJSONBody,
|
||||
RESTPostAPIContextMenuApplicationCommandsJSONBody,
|
||||
} from 'discord.js';
|
||||
|
||||
import { Args } from './index.js';
|
||||
import { Language } from '../models/enum-helpers/index.js';
|
||||
import { Lang } from '../services/index.js';
|
||||
|
||||
export const ChatCommandMetadata: {
|
||||
[command: string]: RESTPostAPIChatInputApplicationCommandsJSONBody;
|
||||
} = {
|
||||
DEV: {
|
||||
type: ApplicationCommandType.ChatInput,
|
||||
name: Lang.getRef('chatCommands.dev', Language.Default),
|
||||
name_localizations: Lang.getRefLocalizationMap('chatCommands.dev'),
|
||||
description: Lang.getRef('commandDescs.dev', Language.Default),
|
||||
description_localizations: Lang.getRefLocalizationMap('commandDescs.dev'),
|
||||
dm_permission: true,
|
||||
default_member_permissions: PermissionsBitField.resolve([
|
||||
PermissionFlagsBits.Administrator,
|
||||
]).toString(),
|
||||
options: [
|
||||
{
|
||||
...Args.DEV_COMMAND,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
HELP: {
|
||||
type: ApplicationCommandType.ChatInput,
|
||||
name: Lang.getRef('chatCommands.help', Language.Default),
|
||||
name_localizations: Lang.getRefLocalizationMap('chatCommands.help'),
|
||||
description: Lang.getRef('commandDescs.help', Language.Default),
|
||||
description_localizations: Lang.getRefLocalizationMap('commandDescs.help'),
|
||||
dm_permission: true,
|
||||
default_member_permissions: undefined,
|
||||
options: [
|
||||
{
|
||||
...Args.HELP_OPTION,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
INFO: {
|
||||
type: ApplicationCommandType.ChatInput,
|
||||
name: Lang.getRef('chatCommands.info', Language.Default),
|
||||
name_localizations: Lang.getRefLocalizationMap('chatCommands.info'),
|
||||
description: Lang.getRef('commandDescs.info', Language.Default),
|
||||
description_localizations: Lang.getRefLocalizationMap('commandDescs.info'),
|
||||
dm_permission: true,
|
||||
default_member_permissions: undefined,
|
||||
options: [
|
||||
{
|
||||
...Args.INFO_OPTION,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
TEST: {
|
||||
type: ApplicationCommandType.ChatInput,
|
||||
name: Lang.getRef('chatCommands.test', Language.Default),
|
||||
name_localizations: Lang.getRefLocalizationMap('chatCommands.test'),
|
||||
description: Lang.getRef('commandDescs.test', Language.Default),
|
||||
description_localizations: Lang.getRefLocalizationMap('commandDescs.test'),
|
||||
dm_permission: true,
|
||||
default_member_permissions: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
export const MessageCommandMetadata: {
|
||||
[command: string]: RESTPostAPIContextMenuApplicationCommandsJSONBody;
|
||||
} = {
|
||||
VIEW_DATE_SENT: {
|
||||
type: ApplicationCommandType.Message,
|
||||
name: Lang.getRef('messageCommands.viewDateSent', Language.Default),
|
||||
name_localizations: Lang.getRefLocalizationMap('messageCommands.viewDateSent'),
|
||||
default_member_permissions: undefined,
|
||||
dm_permission: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const UserCommandMetadata: {
|
||||
[command: string]: RESTPostAPIContextMenuApplicationCommandsJSONBody;
|
||||
} = {
|
||||
VIEW_DATE_JOINED: {
|
||||
type: ApplicationCommandType.User,
|
||||
name: Lang.getRef('userCommands.viewDateJoined', Language.Default),
|
||||
name_localizations: Lang.getRefLocalizationMap('userCommands.viewDateJoined'),
|
||||
default_member_permissions: undefined,
|
||||
dm_permission: true,
|
||||
},
|
||||
};
|
||||
1
src/commands/user/index.ts
Normal file
1
src/commands/user/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { ViewDateJoined } from './view-date-joined.js';
|
||||
32
src/commands/user/view-date-joined.ts
Normal file
32
src/commands/user/view-date-joined.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { DMChannel, PermissionsString, UserContextMenuCommandInteraction } from 'discord.js';
|
||||
import { RateLimiter } from 'discord.js-rate-limiter';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { Language } from '../../models/enum-helpers/index.js';
|
||||
import { EventData } from '../../models/internal-models.js';
|
||||
import { Lang } from '../../services/index.js';
|
||||
import { InteractionUtils } from '../../utils/index.js';
|
||||
import { Command, CommandDeferType } from '../index.js';
|
||||
|
||||
export class ViewDateJoined implements Command {
|
||||
public names = [Lang.getRef('userCommands.viewDateJoined', Language.Default)];
|
||||
public cooldown = new RateLimiter(1, 5000);
|
||||
public deferType = CommandDeferType.HIDDEN;
|
||||
public requireClientPerms: PermissionsString[] = [];
|
||||
|
||||
public async execute(intr: UserContextMenuCommandInteraction, data: EventData): Promise<void> {
|
||||
let joinDate: Date;
|
||||
if (!(intr.channel instanceof DMChannel)) {
|
||||
let member = await intr.guild.members.fetch(intr.targetUser.id);
|
||||
joinDate = member.joinedAt;
|
||||
} else joinDate = intr.targetUser.createdAt;
|
||||
|
||||
await InteractionUtils.send(
|
||||
intr,
|
||||
Lang.getEmbed('displayEmbeds.viewDateJoined', data.lang, {
|
||||
TARGET: intr.targetUser.toString(),
|
||||
DATE: DateTime.fromJSDate(joinDate).toLocaleString(DateTime.DATE_HUGE),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
15
src/constants/discord-limits.ts
Normal file
15
src/constants/discord-limits.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export class DiscordLimits {
|
||||
public static readonly GUILDS_PER_SHARD = 2500;
|
||||
public static readonly CHANNELS_PER_GUILD = 500;
|
||||
public static readonly ROLES_PER_GUILD = 250;
|
||||
public static readonly PINS_PER_CHANNEL = 50;
|
||||
public static readonly ACTIVE_THREADS_PER_GUILD = 1000;
|
||||
public static readonly EMBEDS_PER_MESSAGE = 10;
|
||||
public static readonly FIELDS_PER_EMBED = 25;
|
||||
public static readonly CHOICES_PER_AUTOCOMPLETE = 25;
|
||||
public static readonly EMBED_COMBINED_LENGTH = 6000;
|
||||
public static readonly EMBED_TITLE_LENGTH = 256;
|
||||
public static readonly EMBED_DESCRIPTION_LENGTH = 4096;
|
||||
public static readonly EMBED_FIELD_NAME_LENGTH = 256;
|
||||
public static readonly EMBED_FOOTER_LENGTH = 2048;
|
||||
}
|
||||
1
src/constants/index.ts
Normal file
1
src/constants/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { DiscordLimits } from './discord-limits.js';
|
||||
8
src/controllers/controller.ts
Normal file
8
src/controllers/controller.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { Router } from 'express';
|
||||
|
||||
export interface Controller {
|
||||
path: string;
|
||||
router: Router;
|
||||
authToken?: string;
|
||||
register(): void;
|
||||
}
|
||||
37
src/controllers/guilds-controller.ts
Normal file
37
src/controllers/guilds-controller.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { ShardingManager } from 'discord.js';
|
||||
import { Request, Response, Router } from 'express';
|
||||
import router from 'express-promise-router';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import { Controller } from './index.js';
|
||||
import { GetGuildsResponse } from '../models/cluster-api/index.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
let Config = require('../../config/config.json');
|
||||
|
||||
export class GuildsController implements Controller {
|
||||
public path = '/guilds';
|
||||
public router: Router = router();
|
||||
public authToken: string = Config.api.secret;
|
||||
|
||||
constructor(private shardManager: ShardingManager) {}
|
||||
|
||||
public register(): void {
|
||||
this.router.get('/', (req, res) => this.getGuilds(req, res));
|
||||
}
|
||||
|
||||
private async getGuilds(req: Request, res: Response): Promise<void> {
|
||||
let guilds: string[] = [
|
||||
...new Set(
|
||||
(
|
||||
await this.shardManager.broadcastEval(client => [...client.guilds.cache.keys()])
|
||||
).flat()
|
||||
),
|
||||
];
|
||||
|
||||
let resBody: GetGuildsResponse = {
|
||||
guilds,
|
||||
};
|
||||
res.status(200).json(resBody);
|
||||
}
|
||||
}
|
||||
4
src/controllers/index.ts
Normal file
4
src/controllers/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export { Controller } from './controller.js';
|
||||
export { GuildsController } from './guilds-controller.js';
|
||||
export { ShardsController } from './shards-controller.js';
|
||||
export { RootController } from './root-controller.js';
|
||||
17
src/controllers/root-controller.ts
Normal file
17
src/controllers/root-controller.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { Request, Response, Router } from 'express';
|
||||
import router from 'express-promise-router';
|
||||
|
||||
import { Controller } from './index.js';
|
||||
|
||||
export class RootController implements Controller {
|
||||
public path = '/';
|
||||
public router: Router = router();
|
||||
|
||||
public register(): void {
|
||||
this.router.get('/', (req, res) => this.get(req, res));
|
||||
}
|
||||
|
||||
private async get(req: Request, res: Response): Promise<void> {
|
||||
res.status(200).json({ name: 'Discord Bot Cluster API', author: 'Kevin Novak' });
|
||||
}
|
||||
}
|
||||
80
src/controllers/shards-controller.ts
Normal file
80
src/controllers/shards-controller.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { ActivityType, ShardingManager } from 'discord.js';
|
||||
import { Request, Response, Router } from 'express';
|
||||
import router from 'express-promise-router';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import { Controller } from './index.js';
|
||||
import { CustomClient } from '../extensions/index.js';
|
||||
import { mapClass } from '../middleware/index.js';
|
||||
import {
|
||||
GetShardsResponse,
|
||||
SetShardPresencesRequest,
|
||||
ShardInfo,
|
||||
ShardStats,
|
||||
} from '../models/cluster-api/index.js';
|
||||
import { Logger } from '../services/index.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
let Config = require('../../config/config.json');
|
||||
let Logs = require('../../lang/logs.json');
|
||||
|
||||
export class ShardsController implements Controller {
|
||||
public path = '/shards';
|
||||
public router: Router = router();
|
||||
public authToken: string = Config.api.secret;
|
||||
|
||||
constructor(private shardManager: ShardingManager) {}
|
||||
|
||||
public register(): void {
|
||||
this.router.get('/', (req, res) => this.getShards(req, res));
|
||||
this.router.put('/presence', mapClass(SetShardPresencesRequest), (req, res) =>
|
||||
this.setShardPresences(req, res)
|
||||
);
|
||||
}
|
||||
|
||||
private async getShards(req: Request, res: Response): Promise<void> {
|
||||
let shardDatas = await Promise.all(
|
||||
this.shardManager.shards.map(async shard => {
|
||||
let shardInfo: ShardInfo = {
|
||||
id: shard.id,
|
||||
ready: shard.ready,
|
||||
error: false,
|
||||
};
|
||||
|
||||
try {
|
||||
let uptime = (await shard.fetchClientValue('uptime')) as number;
|
||||
shardInfo.uptimeSecs = Math.floor(uptime / 1000);
|
||||
} catch (error) {
|
||||
Logger.error(Logs.error.managerShardInfo, error);
|
||||
shardInfo.error = true;
|
||||
}
|
||||
|
||||
return shardInfo;
|
||||
})
|
||||
);
|
||||
|
||||
let stats: ShardStats = {
|
||||
shardCount: this.shardManager.shards.size,
|
||||
uptimeSecs: Math.floor(process.uptime()),
|
||||
};
|
||||
|
||||
let resBody: GetShardsResponse = {
|
||||
shards: shardDatas,
|
||||
stats,
|
||||
};
|
||||
res.status(200).json(resBody);
|
||||
}
|
||||
|
||||
private async setShardPresences(req: Request, res: Response): Promise<void> {
|
||||
let reqBody: SetShardPresencesRequest = res.locals.input;
|
||||
|
||||
await this.shardManager.broadcastEval(
|
||||
(client: CustomClient, context) => {
|
||||
return client.setPresence(context.type, context.name, context.url);
|
||||
},
|
||||
{ context: { type: ActivityType[reqBody.type], name: reqBody.name, url: reqBody.url } }
|
||||
);
|
||||
|
||||
res.sendStatus(200);
|
||||
}
|
||||
}
|
||||
3
src/enums/dev-command-name.ts
Normal file
3
src/enums/dev-command-name.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export enum DevCommandName {
|
||||
INFO = 'INFO',
|
||||
}
|
||||
4
src/enums/help-option.ts
Normal file
4
src/enums/help-option.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export enum HelpOption {
|
||||
CONTACT_SUPPORT = 'CONTACT_SUPPORT',
|
||||
COMMANDS = 'COMMANDS',
|
||||
}
|
||||
3
src/enums/index.ts
Normal file
3
src/enums/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { DevCommandName } from './dev-command-name.js';
|
||||
export { HelpOption } from './help-option.js';
|
||||
export { InfoOption } from './info-option.js';
|
||||
4
src/enums/info-option.ts
Normal file
4
src/enums/info-option.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export enum InfoOption {
|
||||
ABOUT = 'ABOUT',
|
||||
TRANSLATE = 'TRANSLATE',
|
||||
}
|
||||
83
src/events/button-handler.ts
Normal file
83
src/events/button-handler.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { ButtonInteraction } from 'discord.js';
|
||||
import { RateLimiter } from 'discord.js-rate-limiter';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import { EventHandler } from './index.js';
|
||||
import { Button, ButtonDeferType } from '../buttons/index.js';
|
||||
import { EventDataService } from '../services/index.js';
|
||||
import { InteractionUtils } from '../utils/index.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
let Config = require('../../config/config.json');
|
||||
|
||||
export class ButtonHandler implements EventHandler {
|
||||
private rateLimiter = new RateLimiter(
|
||||
Config.rateLimiting.buttons.amount,
|
||||
Config.rateLimiting.buttons.interval * 1000
|
||||
);
|
||||
|
||||
constructor(private buttons: Button[], private eventDataService: EventDataService) {}
|
||||
|
||||
public async process(intr: ButtonInteraction): Promise<void> {
|
||||
// Don't respond to self, or other bots
|
||||
if (intr.user.id === intr.client.user?.id || intr.user.bot) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user is rate limited
|
||||
let limited = this.rateLimiter.take(intr.user.id);
|
||||
if (limited) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to find the button the user wants
|
||||
let button = this.findButton(intr.customId);
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (button.requireGuild && !intr.guild) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the embeds author equals the users tag
|
||||
if (
|
||||
button.requireEmbedAuthorTag &&
|
||||
intr.message.embeds[0]?.author?.name !== intr.user.tag
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Defer interaction
|
||||
// NOTE: Anything after this point we should be responding to the interaction
|
||||
switch (button.deferType) {
|
||||
case ButtonDeferType.REPLY: {
|
||||
await InteractionUtils.deferReply(intr);
|
||||
break;
|
||||
}
|
||||
case ButtonDeferType.UPDATE: {
|
||||
await InteractionUtils.deferUpdate(intr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Return if defer was unsuccessful
|
||||
if (button.deferType !== ButtonDeferType.NONE && !intr.deferred) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get data from database
|
||||
let data = await this.eventDataService.create({
|
||||
user: intr.user,
|
||||
channel: intr.channel,
|
||||
guild: intr.guild,
|
||||
});
|
||||
|
||||
// Execute the button
|
||||
await button.execute(intr, data);
|
||||
}
|
||||
|
||||
private findButton(id: string): Button {
|
||||
return this.buttons.find(button => button.ids.includes(id));
|
||||
}
|
||||
}
|
||||
182
src/events/command-handler.ts
Normal file
182
src/events/command-handler.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import {
|
||||
AutocompleteInteraction,
|
||||
ChatInputCommandInteraction,
|
||||
CommandInteraction,
|
||||
NewsChannel,
|
||||
TextChannel,
|
||||
ThreadChannel,
|
||||
} from 'discord.js';
|
||||
import { RateLimiter } from 'discord.js-rate-limiter';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import { EventHandler } from './index.js';
|
||||
import { Command, CommandDeferType } from '../commands/index.js';
|
||||
import { DiscordLimits } from '../constants/index.js';
|
||||
import { EventData } from '../models/internal-models.js';
|
||||
import { EventDataService, Lang, Logger } from '../services/index.js';
|
||||
import { CommandUtils, InteractionUtils } from '../utils/index.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
let Config = require('../../config/config.json');
|
||||
let Logs = require('../../lang/logs.json');
|
||||
|
||||
export class CommandHandler implements EventHandler {
|
||||
private rateLimiter = new RateLimiter(
|
||||
Config.rateLimiting.commands.amount,
|
||||
Config.rateLimiting.commands.interval * 1000
|
||||
);
|
||||
|
||||
constructor(public commands: Command[], private eventDataService: EventDataService) {}
|
||||
|
||||
public async process(intr: CommandInteraction | AutocompleteInteraction): Promise<void> {
|
||||
// Don't respond to self, or other bots
|
||||
if (intr.user.id === intr.client.user?.id || intr.user.bot) {
|
||||
return;
|
||||
}
|
||||
|
||||
let commandParts =
|
||||
intr instanceof ChatInputCommandInteraction || intr instanceof AutocompleteInteraction
|
||||
? [
|
||||
intr.commandName,
|
||||
intr.options.getSubcommandGroup(false),
|
||||
intr.options.getSubcommand(false),
|
||||
].filter(Boolean)
|
||||
: [intr.commandName];
|
||||
let commandName = commandParts.join(' ');
|
||||
|
||||
// Try to find the command the user wants
|
||||
let command = CommandUtils.findCommand(this.commands, commandParts);
|
||||
if (!command) {
|
||||
Logger.error(
|
||||
Logs.error.commandNotFound
|
||||
.replaceAll('{INTERACTION_ID}', intr.id)
|
||||
.replaceAll('{COMMAND_NAME}', commandName)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (intr instanceof AutocompleteInteraction) {
|
||||
if (!command.autocomplete) {
|
||||
Logger.error(
|
||||
Logs.error.autocompleteNotFound
|
||||
.replaceAll('{INTERACTION_ID}', intr.id)
|
||||
.replaceAll('{COMMAND_NAME}', commandName)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let option = intr.options.getFocused(true);
|
||||
let choices = await command.autocomplete(intr, option);
|
||||
await InteractionUtils.respond(
|
||||
intr,
|
||||
choices?.slice(0, DiscordLimits.CHOICES_PER_AUTOCOMPLETE)
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.error(
|
||||
intr.channel instanceof TextChannel ||
|
||||
intr.channel instanceof NewsChannel ||
|
||||
intr.channel instanceof ThreadChannel
|
||||
? Logs.error.autocompleteGuild
|
||||
.replaceAll('{INTERACTION_ID}', intr.id)
|
||||
.replaceAll('{OPTION_NAME}', commandName)
|
||||
.replaceAll('{COMMAND_NAME}', commandName)
|
||||
.replaceAll('{USER_TAG}', intr.user.tag)
|
||||
.replaceAll('{USER_ID}', intr.user.id)
|
||||
.replaceAll('{CHANNEL_NAME}', intr.channel.name)
|
||||
.replaceAll('{CHANNEL_ID}', intr.channel.id)
|
||||
.replaceAll('{GUILD_NAME}', intr.guild?.name)
|
||||
.replaceAll('{GUILD_ID}', intr.guild?.id)
|
||||
: Logs.error.autocompleteOther
|
||||
.replaceAll('{INTERACTION_ID}', intr.id)
|
||||
.replaceAll('{OPTION_NAME}', commandName)
|
||||
.replaceAll('{COMMAND_NAME}', commandName)
|
||||
.replaceAll('{USER_TAG}', intr.user.tag)
|
||||
.replaceAll('{USER_ID}', intr.user.id),
|
||||
error
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user is rate limited
|
||||
let limited = this.rateLimiter.take(intr.user.id);
|
||||
if (limited) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Defer interaction
|
||||
// NOTE: Anything after this point we should be responding to the interaction
|
||||
switch (command.deferType) {
|
||||
case CommandDeferType.PUBLIC: {
|
||||
await InteractionUtils.deferReply(intr, false);
|
||||
break;
|
||||
}
|
||||
case CommandDeferType.HIDDEN: {
|
||||
await InteractionUtils.deferReply(intr, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Return if defer was unsuccessful
|
||||
if (command.deferType !== CommandDeferType.NONE && !intr.deferred) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get data from database
|
||||
let data = await this.eventDataService.create({
|
||||
user: intr.user,
|
||||
channel: intr.channel,
|
||||
guild: intr.guild,
|
||||
args: intr instanceof ChatInputCommandInteraction ? intr.options : undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
// Check if interaction passes command checks
|
||||
let passesChecks = await CommandUtils.runChecks(command, intr, data);
|
||||
if (passesChecks) {
|
||||
// Execute the command
|
||||
await command.execute(intr, data);
|
||||
}
|
||||
} catch (error) {
|
||||
await this.sendError(intr, data);
|
||||
|
||||
// Log command error
|
||||
Logger.error(
|
||||
intr.channel instanceof TextChannel ||
|
||||
intr.channel instanceof NewsChannel ||
|
||||
intr.channel instanceof ThreadChannel
|
||||
? Logs.error.commandGuild
|
||||
.replaceAll('{INTERACTION_ID}', intr.id)
|
||||
.replaceAll('{COMMAND_NAME}', commandName)
|
||||
.replaceAll('{USER_TAG}', intr.user.tag)
|
||||
.replaceAll('{USER_ID}', intr.user.id)
|
||||
.replaceAll('{CHANNEL_NAME}', intr.channel.name)
|
||||
.replaceAll('{CHANNEL_ID}', intr.channel.id)
|
||||
.replaceAll('{GUILD_NAME}', intr.guild?.name)
|
||||
.replaceAll('{GUILD_ID}', intr.guild?.id)
|
||||
: Logs.error.commandOther
|
||||
.replaceAll('{INTERACTION_ID}', intr.id)
|
||||
.replaceAll('{COMMAND_NAME}', commandName)
|
||||
.replaceAll('{USER_TAG}', intr.user.tag)
|
||||
.replaceAll('{USER_ID}', intr.user.id),
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendError(intr: CommandInteraction, data: EventData): Promise<void> {
|
||||
try {
|
||||
await InteractionUtils.send(
|
||||
intr,
|
||||
Lang.getEmbed('errorEmbeds.command', data.lang, {
|
||||
ERROR_CODE: intr.id,
|
||||
GUILD_ID: intr.guild?.id ?? Lang.getRef('other.na', data.lang),
|
||||
SHARD_ID: (intr.guild?.shardId ?? 0).toString(),
|
||||
})
|
||||
);
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
3
src/events/event-handler.ts
Normal file
3
src/events/event-handler.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export interface EventHandler {
|
||||
process(...args: any[]): Promise<void>;
|
||||
}
|
||||
67
src/events/guild-join-handler.ts
Normal file
67
src/events/guild-join-handler.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { Guild } from 'discord.js';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import { EventHandler } from './index.js';
|
||||
import { Language } from '../models/enum-helpers/index.js';
|
||||
import { EventDataService, Lang, Logger } from '../services/index.js';
|
||||
import { ClientUtils, FormatUtils, MessageUtils } from '../utils/index.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
let Logs = require('../../lang/logs.json');
|
||||
|
||||
export class GuildJoinHandler implements EventHandler {
|
||||
constructor(private eventDataService: EventDataService) {}
|
||||
|
||||
public async process(guild: Guild): Promise<void> {
|
||||
Logger.info(
|
||||
Logs.info.guildJoined
|
||||
.replaceAll('{GUILD_NAME}', guild.name)
|
||||
.replaceAll('{GUILD_ID}', guild.id)
|
||||
);
|
||||
|
||||
let owner = await guild.fetchOwner();
|
||||
|
||||
// Get data from database
|
||||
let data = await this.eventDataService.create({
|
||||
user: owner?.user,
|
||||
guild,
|
||||
});
|
||||
|
||||
// Send welcome message to the server's notify channel
|
||||
let notifyChannel = await ClientUtils.findNotifyChannel(guild, data.langGuild);
|
||||
if (notifyChannel) {
|
||||
await MessageUtils.send(
|
||||
notifyChannel,
|
||||
Lang.getEmbed('displayEmbeds.welcome', data.langGuild, {
|
||||
CMD_LINK_HELP: FormatUtils.commandMention(
|
||||
await ClientUtils.findAppCommand(
|
||||
guild.client,
|
||||
Lang.getRef('chatCommands.help', Language.Default)
|
||||
)
|
||||
),
|
||||
}).setAuthor({
|
||||
name: guild.name,
|
||||
iconURL: guild.iconURL(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Send welcome message to owner
|
||||
if (owner) {
|
||||
await MessageUtils.send(
|
||||
owner.user,
|
||||
Lang.getEmbed('displayEmbeds.welcome', data.lang, {
|
||||
CMD_LINK_HELP: FormatUtils.commandMention(
|
||||
await ClientUtils.findAppCommand(
|
||||
guild.client,
|
||||
Lang.getRef('chatCommands.help', Language.Default)
|
||||
)
|
||||
),
|
||||
}).setAuthor({
|
||||
name: guild.name,
|
||||
iconURL: guild.iconURL(),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/events/guild-leave-handler.ts
Normal file
18
src/events/guild-leave-handler.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Guild } from 'discord.js';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import { EventHandler } from './index.js';
|
||||
import { Logger } from '../services/index.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
let Logs = require('../../lang/logs.json');
|
||||
|
||||
export class GuildLeaveHandler implements EventHandler {
|
||||
public async process(guild: Guild): Promise<void> {
|
||||
Logger.info(
|
||||
Logs.info.guildLeft
|
||||
.replaceAll('{GUILD_NAME}', guild.name)
|
||||
.replaceAll('{GUILD_ID}', guild.id)
|
||||
);
|
||||
}
|
||||
}
|
||||
8
src/events/index.ts
Normal file
8
src/events/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export { ButtonHandler } from './button-handler.js';
|
||||
export { CommandHandler } from './command-handler.js';
|
||||
export { EventHandler } from './event-handler.js';
|
||||
export { GuildJoinHandler } from './guild-join-handler.js';
|
||||
export { GuildLeaveHandler } from './guild-leave-handler.js';
|
||||
export { ReactionHandler } from './reaction-handler.js';
|
||||
export { MessageHandler } from './message-handler.js';
|
||||
export { TriggerHandler } from './trigger-handler.js';
|
||||
17
src/events/message-handler.ts
Normal file
17
src/events/message-handler.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { Message } from 'discord.js';
|
||||
|
||||
import { EventHandler, TriggerHandler } from './index.js';
|
||||
|
||||
export class MessageHandler implements EventHandler {
|
||||
constructor(private triggerHandler: TriggerHandler) {}
|
||||
|
||||
public async process(msg: Message): Promise<void> {
|
||||
// Don't respond to system messages or self
|
||||
if (msg.system || msg.author.id === msg.client.user?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Process trigger
|
||||
await this.triggerHandler.process(msg);
|
||||
}
|
||||
}
|
||||
65
src/events/reaction-handler.ts
Normal file
65
src/events/reaction-handler.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { Message, MessageReaction, User } from 'discord.js';
|
||||
import { RateLimiter } from 'discord.js-rate-limiter';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import { EventHandler } from './index.js';
|
||||
import { Reaction } from '../reactions/index.js';
|
||||
import { EventDataService } from '../services/index.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
let Config = require('../../config/config.json');
|
||||
|
||||
export class ReactionHandler implements EventHandler {
|
||||
private rateLimiter = new RateLimiter(
|
||||
Config.rateLimiting.reactions.amount,
|
||||
Config.rateLimiting.reactions.interval * 1000
|
||||
);
|
||||
|
||||
constructor(private reactions: Reaction[], private eventDataService: EventDataService) {}
|
||||
|
||||
public async process(msgReaction: MessageReaction, msg: Message, reactor: User): Promise<void> {
|
||||
// Don't respond to self, or other bots
|
||||
if (reactor.id === msgReaction.client.user?.id || reactor.bot) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user is rate limited
|
||||
let limited = this.rateLimiter.take(msg.author.id);
|
||||
if (limited) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to find the reaction the user wants
|
||||
let reaction = this.findReaction(msgReaction.emoji.name);
|
||||
if (!reaction) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (reaction.requireGuild && !msg.guild) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (reaction.requireSentByClient && msg.author.id !== msg.client.user?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the embeds author equals the reactors tag
|
||||
if (reaction.requireEmbedAuthorTag && msg.embeds[0]?.author?.name !== reactor.tag) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get data from database
|
||||
let data = await this.eventDataService.create({
|
||||
user: reactor,
|
||||
channel: msg.channel,
|
||||
guild: msg.guild,
|
||||
});
|
||||
|
||||
// Execute the reaction
|
||||
await reaction.execute(msgReaction, msg, reactor, data);
|
||||
}
|
||||
|
||||
private findReaction(emoji: string): Reaction {
|
||||
return this.reactions.find(reaction => reaction.emoji === emoji);
|
||||
}
|
||||
}
|
||||
56
src/events/trigger-handler.ts
Normal file
56
src/events/trigger-handler.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { Message } from 'discord.js';
|
||||
import { RateLimiter } from 'discord.js-rate-limiter';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import { EventDataService } from '../services/index.js';
|
||||
import { Trigger } from '../triggers/index.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
let Config = require('../../config/config.json');
|
||||
|
||||
export class TriggerHandler {
|
||||
private rateLimiter = new RateLimiter(
|
||||
Config.rateLimiting.triggers.amount,
|
||||
Config.rateLimiting.triggers.interval * 1000
|
||||
);
|
||||
|
||||
constructor(private triggers: Trigger[], private eventDataService: EventDataService) {}
|
||||
|
||||
public async process(msg: Message): Promise<void> {
|
||||
// Check if user is rate limited
|
||||
let limited = this.rateLimiter.take(msg.author.id);
|
||||
if (limited) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find triggers caused by this message
|
||||
let triggers = this.triggers.filter(trigger => {
|
||||
if (trigger.requireGuild && !msg.guild) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!trigger.triggered(msg)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// If this message causes no triggers then return
|
||||
if (triggers.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get data from database
|
||||
let data = await this.eventDataService.create({
|
||||
user: msg.author,
|
||||
channel: msg.channel,
|
||||
guild: msg.guild,
|
||||
});
|
||||
|
||||
// Execute triggers
|
||||
for (let trigger of triggers) {
|
||||
await trigger.execute(msg, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/extensions/custom-client.ts
Normal file
23
src/extensions/custom-client.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { ActivityType, Client, ClientOptions, Presence } from 'discord.js';
|
||||
|
||||
export class CustomClient extends Client {
|
||||
constructor(clientOptions: ClientOptions) {
|
||||
super(clientOptions);
|
||||
}
|
||||
|
||||
public setPresence(
|
||||
type: Exclude<ActivityType, ActivityType.Custom>,
|
||||
name: string,
|
||||
url: string
|
||||
): Presence {
|
||||
return this.user?.setPresence({
|
||||
activities: [
|
||||
{
|
||||
type,
|
||||
name,
|
||||
url,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
1
src/extensions/index.ts
Normal file
1
src/extensions/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { CustomClient } from './custom-client.js';
|
||||
2
src/jobs/index.ts
Normal file
2
src/jobs/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { Job } from './job.js';
|
||||
export { UpdateServerCountJob } from './update-server-count-job.js';
|
||||
8
src/jobs/job.ts
Normal file
8
src/jobs/job.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export abstract class Job {
|
||||
abstract name: string;
|
||||
abstract log: boolean;
|
||||
abstract schedule: string;
|
||||
runOnce = false;
|
||||
initialDelaySecs = 0;
|
||||
abstract run(): Promise<void>;
|
||||
}
|
||||
68
src/jobs/update-server-count-job.ts
Normal file
68
src/jobs/update-server-count-job.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { ActivityType, ShardingManager } from 'discord.js';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import { Job } from './index.js';
|
||||
import { CustomClient } from '../extensions/index.js';
|
||||
import { BotSite } from '../models/config-models.js';
|
||||
import { HttpService, Lang, Logger } from '../services/index.js';
|
||||
import { ShardUtils } from '../utils/index.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
let BotSites: BotSite[] = require('../../config/bot-sites.json');
|
||||
let Config = require('../../config/config.json');
|
||||
let Logs = require('../../lang/logs.json');
|
||||
|
||||
export class UpdateServerCountJob extends Job {
|
||||
public name = 'Update Server Count';
|
||||
public schedule: string = Config.jobs.updateServerCount.schedule;
|
||||
public log: boolean = Config.jobs.updateServerCount.log;
|
||||
public runOnce: boolean = Config.jobs.updateServerCount.runOnce;
|
||||
public initialDelaySecs: number = Config.jobs.updateServerCount.initialDelaySecs;
|
||||
|
||||
private botSites: BotSite[];
|
||||
|
||||
constructor(private shardManager: ShardingManager, private httpService: HttpService) {
|
||||
super();
|
||||
this.botSites = BotSites.filter(botSite => botSite.enabled);
|
||||
}
|
||||
|
||||
public async run(): Promise<void> {
|
||||
let serverCount = await ShardUtils.serverCount(this.shardManager);
|
||||
|
||||
let type = ActivityType.Streaming;
|
||||
let name = `to ${serverCount.toLocaleString()} servers`;
|
||||
let url = Lang.getCom('links.stream');
|
||||
|
||||
await this.shardManager.broadcastEval(
|
||||
(client: CustomClient, context) => {
|
||||
return client.setPresence(context.type, context.name, context.url);
|
||||
},
|
||||
{ context: { type, name, url } }
|
||||
);
|
||||
|
||||
Logger.info(
|
||||
Logs.info.updatedServerCount.replaceAll('{SERVER_COUNT}', serverCount.toLocaleString())
|
||||
);
|
||||
|
||||
for (let botSite of this.botSites) {
|
||||
try {
|
||||
let body = JSON.parse(
|
||||
botSite.body.replaceAll('{{SERVER_COUNT}}', serverCount.toString())
|
||||
);
|
||||
let res = await this.httpService.post(botSite.url, botSite.authorization, body);
|
||||
|
||||
if (!res.ok) {
|
||||
throw res;
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(
|
||||
Logs.error.updatedServerCountSite.replaceAll('{BOT_SITE}', botSite.name),
|
||||
error
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.info(Logs.info.updatedServerCountSite.replaceAll('{BOT_SITE}', botSite.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/middleware/check-auth.ts
Normal file
11
src/middleware/check-auth.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { RequestHandler } from 'express';
|
||||
|
||||
export function checkAuth(token: string): RequestHandler {
|
||||
return (req, res, next) => {
|
||||
if (req.headers.authorization !== token) {
|
||||
res.sendStatus(401);
|
||||
return;
|
||||
}
|
||||
next();
|
||||
};
|
||||
}
|
||||
19
src/middleware/handle-error.ts
Normal file
19
src/middleware/handle-error.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { ErrorRequestHandler } from 'express';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import { Logger } from '../services/index.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
let Logs = require('../../lang/logs.json');
|
||||
|
||||
export function handleError(): ErrorRequestHandler {
|
||||
return (error, req, res, _next) => {
|
||||
Logger.error(
|
||||
Logs.error.apiRequest
|
||||
.replaceAll('{HTTP_METHOD}', req.method)
|
||||
.replaceAll('{URL}', req.url),
|
||||
error
|
||||
);
|
||||
res.status(500).json({ error: true, message: error.message });
|
||||
};
|
||||
}
|
||||
3
src/middleware/index.ts
Normal file
3
src/middleware/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { checkAuth } from './check-auth.js';
|
||||
export { handleError } from './handle-error.js';
|
||||
export { mapClass } from './map-class.js';
|
||||
40
src/middleware/map-class.ts
Normal file
40
src/middleware/map-class.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { ClassConstructor, plainToInstance } from 'class-transformer';
|
||||
import { validate, ValidationError } from 'class-validator';
|
||||
import { NextFunction, Request, RequestHandler, Response } from 'express';
|
||||
|
||||
export function mapClass(cls: ClassConstructor<object>): RequestHandler {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
// Map to class
|
||||
let obj: object = plainToInstance(cls, req.body);
|
||||
|
||||
// Validate class
|
||||
let errors = await validate(obj, {
|
||||
skipMissingProperties: true,
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: false,
|
||||
forbidUnknownValues: true,
|
||||
});
|
||||
if (errors.length > 0) {
|
||||
res.status(400).send({ error: true, errors: formatValidationErrors(errors) });
|
||||
return;
|
||||
}
|
||||
|
||||
// Set validated class to locals
|
||||
res.locals.input = obj;
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
interface ValidationErrorLog {
|
||||
property: string;
|
||||
constraints?: { [type: string]: string };
|
||||
children?: ValidationErrorLog[];
|
||||
}
|
||||
|
||||
function formatValidationErrors(errors: ValidationError[]): ValidationErrorLog[] {
|
||||
return errors.map(error => ({
|
||||
property: error.property,
|
||||
constraints: error.constraints,
|
||||
children: error.children?.length > 0 ? formatValidationErrors(error.children) : undefined,
|
||||
}));
|
||||
}
|
||||
38
src/models/api.ts
Normal file
38
src/models/api.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import express, { Express } from 'express';
|
||||
import { createRequire } from 'node:module';
|
||||
import util from 'node:util';
|
||||
|
||||
import { Controller } from '../controllers/index.js';
|
||||
import { checkAuth, handleError } from '../middleware/index.js';
|
||||
import { Logger } from '../services/index.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
let Config = require('../../config/config.json');
|
||||
let Logs = require('../../lang/logs.json');
|
||||
|
||||
export class Api {
|
||||
private app: Express;
|
||||
|
||||
constructor(public controllers: Controller[]) {
|
||||
this.app = express();
|
||||
this.app.use(express.json());
|
||||
this.setupControllers();
|
||||
this.app.use(handleError());
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
let listen = util.promisify(this.app.listen.bind(this.app));
|
||||
await listen(Config.api.port);
|
||||
Logger.info(Logs.info.apiStarted.replaceAll('{PORT}', Config.api.port));
|
||||
}
|
||||
|
||||
private setupControllers(): void {
|
||||
for (let controller of this.controllers) {
|
||||
if (controller.authToken) {
|
||||
controller.router.use(checkAuth(controller.authToken));
|
||||
}
|
||||
controller.register();
|
||||
this.app.use(controller.path, controller.router);
|
||||
}
|
||||
}
|
||||
}
|
||||
203
src/models/bot.ts
Normal file
203
src/models/bot.ts
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
import {
|
||||
AutocompleteInteraction,
|
||||
ButtonInteraction,
|
||||
Client,
|
||||
CommandInteraction,
|
||||
Events,
|
||||
Guild,
|
||||
Interaction,
|
||||
Message,
|
||||
MessageReaction,
|
||||
PartialMessageReaction,
|
||||
PartialUser,
|
||||
RateLimitData,
|
||||
RESTEvents,
|
||||
User,
|
||||
} from 'discord.js';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import {
|
||||
ButtonHandler,
|
||||
CommandHandler,
|
||||
GuildJoinHandler,
|
||||
GuildLeaveHandler,
|
||||
MessageHandler,
|
||||
ReactionHandler,
|
||||
} from '../events/index.js';
|
||||
import { JobService, Logger } from '../services/index.js';
|
||||
import { PartialUtils } from '../utils/index.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
let Config = require('../../config/config.json');
|
||||
let Debug = require('../../config/debug.json');
|
||||
let Logs = require('../../lang/logs.json');
|
||||
|
||||
export class Bot {
|
||||
private ready = false;
|
||||
|
||||
constructor(
|
||||
private token: string,
|
||||
private client: Client,
|
||||
private guildJoinHandler: GuildJoinHandler,
|
||||
private guildLeaveHandler: GuildLeaveHandler,
|
||||
private messageHandler: MessageHandler,
|
||||
private commandHandler: CommandHandler,
|
||||
private buttonHandler: ButtonHandler,
|
||||
private reactionHandler: ReactionHandler,
|
||||
private jobService: JobService
|
||||
) {}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
this.registerListeners();
|
||||
await this.login(this.token);
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this.client.on(Events.ClientReady, () => this.onReady());
|
||||
this.client.on(Events.ShardReady, (shardId: number, unavailableGuilds: Set<string>) =>
|
||||
this.onShardReady(shardId, unavailableGuilds)
|
||||
);
|
||||
this.client.on(Events.GuildCreate, (guild: Guild) => this.onGuildJoin(guild));
|
||||
this.client.on(Events.GuildDelete, (guild: Guild) => this.onGuildLeave(guild));
|
||||
this.client.on(Events.MessageCreate, (msg: Message) => this.onMessage(msg));
|
||||
this.client.on(Events.InteractionCreate, (intr: Interaction) => this.onInteraction(intr));
|
||||
this.client.on(
|
||||
Events.MessageReactionAdd,
|
||||
(messageReaction: MessageReaction | PartialMessageReaction, user: User | PartialUser) =>
|
||||
this.onReaction(messageReaction, user)
|
||||
);
|
||||
this.client.rest.on(RESTEvents.RateLimited, (rateLimitData: RateLimitData) =>
|
||||
this.onRateLimit(rateLimitData)
|
||||
);
|
||||
}
|
||||
|
||||
private async login(token: string): Promise<void> {
|
||||
try {
|
||||
await this.client.login(token);
|
||||
} catch (error) {
|
||||
Logger.error(Logs.error.clientLogin, error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async onReady(): Promise<void> {
|
||||
let userTag = this.client.user?.tag;
|
||||
Logger.info(Logs.info.clientLogin.replaceAll('{USER_TAG}', userTag));
|
||||
|
||||
if (!Debug.dummyMode.enabled) {
|
||||
this.jobService.start();
|
||||
}
|
||||
|
||||
this.ready = true;
|
||||
Logger.info(Logs.info.clientReady);
|
||||
}
|
||||
|
||||
private onShardReady(shardId: number, _unavailableGuilds: Set<string>): void {
|
||||
Logger.setShardId(shardId);
|
||||
}
|
||||
|
||||
private async onGuildJoin(guild: Guild): Promise<void> {
|
||||
if (!this.ready || Debug.dummyMode.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.guildJoinHandler.process(guild);
|
||||
} catch (error) {
|
||||
Logger.error(Logs.error.guildJoin, error);
|
||||
}
|
||||
}
|
||||
|
||||
private async onGuildLeave(guild: Guild): Promise<void> {
|
||||
if (!this.ready || Debug.dummyMode.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.guildLeaveHandler.process(guild);
|
||||
} catch (error) {
|
||||
Logger.error(Logs.error.guildLeave, error);
|
||||
}
|
||||
}
|
||||
|
||||
private async onMessage(msg: Message): Promise<void> {
|
||||
if (
|
||||
!this.ready ||
|
||||
(Debug.dummyMode.enabled && !Debug.dummyMode.whitelist.includes(msg.author.id))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
msg = await PartialUtils.fillMessage(msg);
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.messageHandler.process(msg);
|
||||
} catch (error) {
|
||||
Logger.error(Logs.error.message, error);
|
||||
}
|
||||
}
|
||||
|
||||
private async onInteraction(intr: Interaction): Promise<void> {
|
||||
if (
|
||||
!this.ready ||
|
||||
(Debug.dummyMode.enabled && !Debug.dummyMode.whitelist.includes(intr.user.id))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (intr instanceof CommandInteraction || intr instanceof AutocompleteInteraction) {
|
||||
try {
|
||||
await this.commandHandler.process(intr);
|
||||
} catch (error) {
|
||||
Logger.error(Logs.error.command, error);
|
||||
}
|
||||
} else if (intr instanceof ButtonInteraction) {
|
||||
try {
|
||||
await this.buttonHandler.process(intr);
|
||||
} catch (error) {
|
||||
Logger.error(Logs.error.button, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async onReaction(
|
||||
msgReaction: MessageReaction | PartialMessageReaction,
|
||||
reactor: User | PartialUser
|
||||
): Promise<void> {
|
||||
if (
|
||||
!this.ready ||
|
||||
(Debug.dummyMode.enabled && !Debug.dummyMode.whitelist.includes(reactor.id))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
msgReaction = await PartialUtils.fillReaction(msgReaction);
|
||||
if (!msgReaction) {
|
||||
return;
|
||||
}
|
||||
|
||||
reactor = await PartialUtils.fillUser(reactor);
|
||||
if (!reactor) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.reactionHandler.process(
|
||||
msgReaction,
|
||||
msgReaction.message as Message,
|
||||
reactor
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.error(Logs.error.reaction, error);
|
||||
}
|
||||
}
|
||||
|
||||
private async onRateLimit(rateLimitData: RateLimitData): Promise<void> {
|
||||
if (rateLimitData.timeToReset >= Config.logging.rateLimit.minTimeout * 1000) {
|
||||
Logger.error(Logs.error.apiRateLimit, rateLimitData);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
src/models/cluster-api/guilds.ts
Normal file
3
src/models/cluster-api/guilds.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export interface GetGuildsResponse {
|
||||
guilds: string[];
|
||||
}
|
||||
2
src/models/cluster-api/index.ts
Normal file
2
src/models/cluster-api/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { GetGuildsResponse } from './guilds.js';
|
||||
export { GetShardsResponse, ShardInfo, ShardStats, SetShardPresencesRequest } from './shards.js';
|
||||
34
src/models/cluster-api/shards.ts
Normal file
34
src/models/cluster-api/shards.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { IsDefined, IsEnum, IsString, IsUrl, Length } from 'class-validator';
|
||||
import { ActivityType } from 'discord.js';
|
||||
|
||||
export interface GetShardsResponse {
|
||||
shards: ShardInfo[];
|
||||
stats: ShardStats;
|
||||
}
|
||||
|
||||
export interface ShardStats {
|
||||
shardCount: number;
|
||||
uptimeSecs: number;
|
||||
}
|
||||
|
||||
export interface ShardInfo {
|
||||
id: number;
|
||||
ready: boolean;
|
||||
error: boolean;
|
||||
uptimeSecs?: number;
|
||||
}
|
||||
|
||||
export class SetShardPresencesRequest {
|
||||
@IsDefined()
|
||||
@IsEnum(ActivityType)
|
||||
type: string;
|
||||
|
||||
@IsDefined()
|
||||
@IsString()
|
||||
@Length(1, 128)
|
||||
name: string;
|
||||
|
||||
@IsDefined()
|
||||
@IsUrl()
|
||||
url: string;
|
||||
}
|
||||
7
src/models/config-models.ts
Normal file
7
src/models/config-models.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export interface BotSite {
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
url: string;
|
||||
authorization: string;
|
||||
body: string;
|
||||
}
|
||||
2
src/models/enum-helpers/index.ts
Normal file
2
src/models/enum-helpers/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { Language } from './language.js';
|
||||
export { Permission } from './permission.js';
|
||||
110
src/models/enum-helpers/language.ts
Normal file
110
src/models/enum-helpers/language.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { Locale } from 'discord.js';
|
||||
|
||||
interface LanguageData {
|
||||
englishName: string;
|
||||
nativeName: string;
|
||||
}
|
||||
|
||||
export class Language {
|
||||
public static Default = Locale.EnglishUS;
|
||||
public static Enabled: Locale[] = [Locale.EnglishUS, Locale.EnglishGB];
|
||||
|
||||
// See https://discord.com/developers/docs/reference#locales
|
||||
public static Data: {
|
||||
[key in Locale]: LanguageData;
|
||||
} = {
|
||||
bg: { englishName: 'Bulgarian', nativeName: 'български' },
|
||||
cs: { englishName: 'Czech', nativeName: 'Čeština' },
|
||||
da: { englishName: 'Danish', nativeName: 'Dansk' },
|
||||
de: { englishName: 'German', nativeName: 'Deutsch' },
|
||||
el: { englishName: 'Greek', nativeName: 'Ελληνικά' },
|
||||
'en-GB': { englishName: 'English, UK', nativeName: 'English, UK' },
|
||||
'en-US': { englishName: 'English, US', nativeName: 'English, US' },
|
||||
'es-ES': { englishName: 'Spanish', nativeName: 'Español' },
|
||||
fi: { englishName: 'Finnish', nativeName: 'Suomi' },
|
||||
fr: { englishName: 'French', nativeName: 'Français' },
|
||||
hi: { englishName: 'Hindi', nativeName: 'हिन्दी' },
|
||||
hr: { englishName: 'Croatian', nativeName: 'Hrvatski' },
|
||||
hu: { englishName: 'Hungarian', nativeName: 'Magyar' },
|
||||
id: { englishName: 'Indonesian', nativeName: 'Bahasa Indonesia' },
|
||||
it: { englishName: 'Italian', nativeName: 'Italiano' },
|
||||
ja: { englishName: 'Japanese', nativeName: '日本語' },
|
||||
ko: { englishName: 'Korean', nativeName: '한국어' },
|
||||
lt: { englishName: 'Lithuanian', nativeName: 'Lietuviškai' },
|
||||
nl: { englishName: 'Dutch', nativeName: 'Nederlands' },
|
||||
no: { englishName: 'Norwegian', nativeName: 'Norsk' },
|
||||
pl: { englishName: 'Polish', nativeName: 'Polski' },
|
||||
'pt-BR': { englishName: 'Portuguese, Brazilian', nativeName: 'Português do Brasil' },
|
||||
ro: { englishName: 'Romanian, Romania', nativeName: 'Română' },
|
||||
ru: { englishName: 'Russian', nativeName: 'Pусский' },
|
||||
'sv-SE': { englishName: 'Swedish', nativeName: 'Svenska' },
|
||||
th: { englishName: 'Thai', nativeName: 'ไทย' },
|
||||
tr: { englishName: 'Turkish', nativeName: 'Türkçe' },
|
||||
uk: { englishName: 'Ukrainian', nativeName: 'Українська' },
|
||||
vi: { englishName: 'Vietnamese', nativeName: 'Tiếng Việt' },
|
||||
'zh-CN': { englishName: 'Chinese, China', nativeName: '中文' },
|
||||
'zh-TW': { englishName: 'Chinese, Taiwan', nativeName: '繁體中文' },
|
||||
};
|
||||
|
||||
public static find(input: string, enabled: boolean): Locale {
|
||||
return this.findMultiple(input, enabled, 1)[0];
|
||||
}
|
||||
|
||||
public static findMultiple(
|
||||
input: string,
|
||||
enabled: boolean,
|
||||
limit: number = Number.MAX_VALUE
|
||||
): Locale[] {
|
||||
let langCodes = enabled ? this.Enabled : Object.values(Locale).sort();
|
||||
let search = input.toLowerCase();
|
||||
let found = new Set<Locale>();
|
||||
// Exact match
|
||||
if (found.size < limit)
|
||||
langCodes
|
||||
.filter(langCode => langCode.toLowerCase() === search)
|
||||
.forEach(langCode => found.add(langCode));
|
||||
if (found.size < limit)
|
||||
langCodes
|
||||
.filter(langCode => this.Data[langCode].nativeName.toLowerCase() === search)
|
||||
.forEach(langCode => found.add(langCode));
|
||||
if (found.size < limit)
|
||||
langCodes
|
||||
.filter(langCode => this.Data[langCode].nativeName.toLowerCase() === search)
|
||||
.forEach(langCode => found.add(langCode));
|
||||
if (found.size < limit)
|
||||
langCodes
|
||||
.filter(langCode => this.Data[langCode].englishName.toLowerCase() === search)
|
||||
.forEach(langCode => found.add(langCode));
|
||||
// Starts with search term
|
||||
if (found.size < limit)
|
||||
langCodes
|
||||
.filter(langCode => langCode.toLowerCase().startsWith(search))
|
||||
.forEach(langCode => found.add(langCode));
|
||||
if (found.size < limit)
|
||||
langCodes
|
||||
.filter(langCode => this.Data[langCode].nativeName.toLowerCase().startsWith(search))
|
||||
.forEach(langCode => found.add(langCode));
|
||||
if (found.size < limit)
|
||||
langCodes
|
||||
.filter(langCode =>
|
||||
this.Data[langCode].englishName.toLowerCase().startsWith(search)
|
||||
)
|
||||
.forEach(langCode => found.add(langCode));
|
||||
// Includes search term
|
||||
if (found.size < limit)
|
||||
langCodes
|
||||
.filter(langCode => langCode.toLowerCase().startsWith(search))
|
||||
.forEach(langCode => found.add(langCode));
|
||||
if (found.size < limit)
|
||||
langCodes
|
||||
.filter(langCode => this.Data[langCode].nativeName.toLowerCase().startsWith(search))
|
||||
.forEach(langCode => found.add(langCode));
|
||||
if (found.size < limit)
|
||||
langCodes
|
||||
.filter(langCode =>
|
||||
this.Data[langCode].englishName.toLowerCase().startsWith(search)
|
||||
)
|
||||
.forEach(langCode => found.add(langCode));
|
||||
return [...found];
|
||||
}
|
||||
}
|
||||
244
src/models/enum-helpers/permission.ts
Normal file
244
src/models/enum-helpers/permission.ts
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
import { Locale, PermissionsString } from 'discord.js';
|
||||
|
||||
import { Lang } from '../../services/index.js';
|
||||
|
||||
interface PermissionData {
|
||||
displayName(langCode: Locale): string;
|
||||
}
|
||||
|
||||
export class Permission {
|
||||
public static Data: {
|
||||
[key in PermissionsString]: PermissionData;
|
||||
} = {
|
||||
AddReactions: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.AddReactions', langCode);
|
||||
},
|
||||
},
|
||||
Administrator: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.Administrator', langCode);
|
||||
},
|
||||
},
|
||||
AttachFiles: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.AttachFiles', langCode);
|
||||
},
|
||||
},
|
||||
BanMembers: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.BanMembers', langCode);
|
||||
},
|
||||
},
|
||||
ChangeNickname: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.ChangeNickname', langCode);
|
||||
},
|
||||
},
|
||||
Connect: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.Connect', langCode);
|
||||
},
|
||||
},
|
||||
CreateInstantInvite: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.CreateInstantInvite', langCode);
|
||||
},
|
||||
},
|
||||
CreatePrivateThreads: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.CreatePrivateThreads', langCode);
|
||||
},
|
||||
},
|
||||
CreatePublicThreads: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.CreatePublicThreads', langCode);
|
||||
},
|
||||
},
|
||||
DeafenMembers: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.DeafenMembers', langCode);
|
||||
},
|
||||
},
|
||||
EmbedLinks: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.EmbedLinks', langCode);
|
||||
},
|
||||
},
|
||||
KickMembers: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.KickMembers', langCode);
|
||||
},
|
||||
},
|
||||
ManageChannels: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.ManageChannels', langCode);
|
||||
},
|
||||
},
|
||||
ManageEmojisAndStickers: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.ManageEmojisAndStickers', langCode);
|
||||
},
|
||||
},
|
||||
ManageEvents: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.ManageEvents', langCode);
|
||||
},
|
||||
},
|
||||
ManageGuild: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.ManageGuild', langCode);
|
||||
},
|
||||
},
|
||||
ManageGuildExpressions: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.ManageGuildExpressions', langCode);
|
||||
},
|
||||
},
|
||||
ManageMessages: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.ManageMessages', langCode);
|
||||
},
|
||||
},
|
||||
ManageNicknames: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.ManageNicknames', langCode);
|
||||
},
|
||||
},
|
||||
ManageRoles: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.ManageRoles', langCode);
|
||||
},
|
||||
},
|
||||
ManageThreads: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.ManageThreads', langCode);
|
||||
},
|
||||
},
|
||||
ManageWebhooks: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.ManageWebhooks', langCode);
|
||||
},
|
||||
},
|
||||
MentionEveryone: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.MentionEveryone', langCode);
|
||||
},
|
||||
},
|
||||
ModerateMembers: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.ModerateMembers', langCode);
|
||||
},
|
||||
},
|
||||
MoveMembers: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.MoveMembers', langCode);
|
||||
},
|
||||
},
|
||||
MuteMembers: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.MuteMembers', langCode);
|
||||
},
|
||||
},
|
||||
PrioritySpeaker: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.PrioritySpeaker', langCode);
|
||||
},
|
||||
},
|
||||
ReadMessageHistory: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.ReadMessageHistory', langCode);
|
||||
},
|
||||
},
|
||||
RequestToSpeak: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.RequestToSpeak', langCode);
|
||||
},
|
||||
},
|
||||
SendMessages: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.SendMessages', langCode);
|
||||
},
|
||||
},
|
||||
SendMessagesInThreads: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.SendMessagesInThreads', langCode);
|
||||
},
|
||||
},
|
||||
SendTTSMessages: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.SendTTSMessages', langCode);
|
||||
},
|
||||
},
|
||||
SendVoiceMessages: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.SendVoiceMessages', langCode);
|
||||
},
|
||||
},
|
||||
Speak: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.Speak', langCode);
|
||||
},
|
||||
},
|
||||
Stream: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.Stream', langCode);
|
||||
},
|
||||
},
|
||||
UseApplicationCommands: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.UseApplicationCommands', langCode);
|
||||
},
|
||||
},
|
||||
UseEmbeddedActivities: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.UseEmbeddedActivities', langCode);
|
||||
},
|
||||
},
|
||||
UseExternalEmojis: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.UseExternalEmojis', langCode);
|
||||
},
|
||||
},
|
||||
UseExternalSounds: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.UseExternalSounds', langCode);
|
||||
},
|
||||
},
|
||||
UseExternalStickers: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.UseExternalStickers', langCode);
|
||||
},
|
||||
},
|
||||
UseSoundboard: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.UseSoundboard', langCode);
|
||||
},
|
||||
},
|
||||
UseVAD: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.UseVAD', langCode);
|
||||
},
|
||||
},
|
||||
ViewAuditLog: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.ViewAuditLog', langCode);
|
||||
},
|
||||
},
|
||||
ViewChannel: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.ViewChannel', langCode);
|
||||
},
|
||||
},
|
||||
ViewCreatorMonetizationAnalytics: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.ViewCreatorMonetizationAnalytics', langCode);
|
||||
},
|
||||
},
|
||||
ViewGuildInsights: {
|
||||
displayName(langCode: Locale): string {
|
||||
return Lang.getRef('permissions.ViewGuildInsights', langCode);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
12
src/models/internal-models.ts
Normal file
12
src/models/internal-models.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Locale } from 'discord.js';
|
||||
|
||||
// This class is used to store and pass data along in events
|
||||
export class EventData {
|
||||
// TODO: Add any data you want to store
|
||||
constructor(
|
||||
// Event language
|
||||
public lang: Locale,
|
||||
// Guild language
|
||||
public langGuild: Locale
|
||||
) {}
|
||||
}
|
||||
50
src/models/manager.ts
Normal file
50
src/models/manager.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { Shard, ShardingManager } from 'discord.js';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import { JobService, Logger } from '../services/index.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
let Config = require('../../config/config.json');
|
||||
let Debug = require('../../config/debug.json');
|
||||
let Logs = require('../../lang/logs.json');
|
||||
|
||||
export class Manager {
|
||||
constructor(private shardManager: ShardingManager, private jobService: JobService) {}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
this.registerListeners();
|
||||
|
||||
let shardList = this.shardManager.shardList as number[];
|
||||
|
||||
try {
|
||||
Logger.info(
|
||||
Logs.info.managerSpawningShards
|
||||
.replaceAll('{SHARD_COUNT}', shardList.length.toLocaleString())
|
||||
.replaceAll('{SHARD_LIST}', shardList.join(', '))
|
||||
);
|
||||
await this.shardManager.spawn({
|
||||
amount: this.shardManager.totalShards,
|
||||
delay: Config.sharding.spawnDelay * 1000,
|
||||
timeout: Config.sharding.spawnTimeout * 1000,
|
||||
});
|
||||
Logger.info(Logs.info.managerAllShardsSpawned);
|
||||
} catch (error) {
|
||||
Logger.error(Logs.error.managerSpawningShards, error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Debug.dummyMode.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.jobService.start();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this.shardManager.on('shardCreate', shard => this.onShardCreate(shard));
|
||||
}
|
||||
|
||||
private onShardCreate(shard: Shard): void {
|
||||
Logger.info(Logs.info.managerLaunchedShard.replaceAll('{SHARD_ID}', shard.id.toString()));
|
||||
}
|
||||
}
|
||||
42
src/models/master-api/clusters.ts
Normal file
42
src/models/master-api/clusters.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsDefined,
|
||||
IsInt,
|
||||
IsPositive,
|
||||
IsString,
|
||||
IsUrl,
|
||||
Length,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
export class Callback {
|
||||
@IsDefined()
|
||||
@IsUrl({ require_tld: false })
|
||||
url: string;
|
||||
|
||||
@IsDefined()
|
||||
@IsString()
|
||||
@Length(5, 2000)
|
||||
token: string;
|
||||
}
|
||||
|
||||
export class RegisterClusterRequest {
|
||||
@IsDefined()
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
shardCount: number;
|
||||
|
||||
@IsDefined()
|
||||
@ValidateNested()
|
||||
@Type(() => Callback)
|
||||
callback: Callback;
|
||||
}
|
||||
|
||||
export interface RegisterClusterResponse {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface LoginClusterResponse {
|
||||
shardList: number[];
|
||||
totalShards: number;
|
||||
}
|
||||
5
src/models/master-api/index.ts
Normal file
5
src/models/master-api/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export {
|
||||
RegisterClusterRequest,
|
||||
RegisterClusterResponse,
|
||||
LoginClusterResponse,
|
||||
} from './clusters.js';
|
||||
1
src/reactions/index.ts
Normal file
1
src/reactions/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { Reaction } from './reaction.js';
|
||||
16
src/reactions/reaction.ts
Normal file
16
src/reactions/reaction.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { Message, MessageReaction, User } from 'discord.js';
|
||||
|
||||
import { EventData } from '../models/internal-models.js';
|
||||
|
||||
export interface Reaction {
|
||||
emoji: string;
|
||||
requireGuild: boolean;
|
||||
requireSentByClient: boolean;
|
||||
requireEmbedAuthorTag: boolean;
|
||||
execute(
|
||||
msgReaction: MessageReaction,
|
||||
msg: Message,
|
||||
reactor: User,
|
||||
data: EventData
|
||||
): Promise<void>;
|
||||
}
|
||||
157
src/services/command-registration-service.ts
Normal file
157
src/services/command-registration-service.ts
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import { REST } from '@discordjs/rest';
|
||||
import {
|
||||
APIApplicationCommand,
|
||||
RESTGetAPIApplicationCommandsResult,
|
||||
RESTPatchAPIApplicationCommandJSONBody,
|
||||
RESTPostAPIApplicationCommandsJSONBody,
|
||||
Routes,
|
||||
} from 'discord.js';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import { Logger } from './logger.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
let Config = require('../../config/config.json');
|
||||
let Logs = require('../../lang/logs.json');
|
||||
|
||||
export class CommandRegistrationService {
|
||||
constructor(private rest: REST) {}
|
||||
|
||||
public async process(
|
||||
localCmds: RESTPostAPIApplicationCommandsJSONBody[],
|
||||
args: string[]
|
||||
): Promise<void> {
|
||||
let remoteCmds = (await this.rest.get(
|
||||
Routes.applicationCommands(Config.client.id)
|
||||
)) as RESTGetAPIApplicationCommandsResult;
|
||||
|
||||
let localCmdsOnRemote = localCmds.filter(localCmd =>
|
||||
remoteCmds.some(remoteCmd => remoteCmd.name === localCmd.name)
|
||||
);
|
||||
let localCmdsOnly = localCmds.filter(
|
||||
localCmd => !remoteCmds.some(remoteCmd => remoteCmd.name === localCmd.name)
|
||||
);
|
||||
let remoteCmdsOnly = remoteCmds.filter(
|
||||
remoteCmd => !localCmds.some(localCmd => localCmd.name === remoteCmd.name)
|
||||
);
|
||||
|
||||
switch (args[3]) {
|
||||
case 'view': {
|
||||
Logger.info(
|
||||
Logs.info.commandActionView
|
||||
.replaceAll(
|
||||
'{LOCAL_AND_REMOTE_LIST}',
|
||||
this.formatCommandList(localCmdsOnRemote)
|
||||
)
|
||||
.replaceAll('{LOCAL_ONLY_LIST}', this.formatCommandList(localCmdsOnly))
|
||||
.replaceAll('{REMOTE_ONLY_LIST}', this.formatCommandList(remoteCmdsOnly))
|
||||
);
|
||||
return;
|
||||
}
|
||||
case 'register': {
|
||||
if (localCmdsOnly.length > 0) {
|
||||
Logger.info(
|
||||
Logs.info.commandActionCreating.replaceAll(
|
||||
'{COMMAND_LIST}',
|
||||
this.formatCommandList(localCmdsOnly)
|
||||
)
|
||||
);
|
||||
for (let localCmd of localCmdsOnly) {
|
||||
await this.rest.post(Routes.applicationCommands(Config.client.id), {
|
||||
body: localCmd,
|
||||
});
|
||||
}
|
||||
Logger.info(Logs.info.commandActionCreated);
|
||||
}
|
||||
|
||||
if (localCmdsOnRemote.length > 0) {
|
||||
Logger.info(
|
||||
Logs.info.commandActionUpdating.replaceAll(
|
||||
'{COMMAND_LIST}',
|
||||
this.formatCommandList(localCmdsOnRemote)
|
||||
)
|
||||
);
|
||||
for (let localCmd of localCmdsOnRemote) {
|
||||
await this.rest.post(Routes.applicationCommands(Config.client.id), {
|
||||
body: localCmd,
|
||||
});
|
||||
}
|
||||
Logger.info(Logs.info.commandActionUpdated);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
case 'rename': {
|
||||
let oldName = args[4];
|
||||
let newName = args[5];
|
||||
if (!(oldName && newName)) {
|
||||
Logger.error(Logs.error.commandActionRenameMissingArg);
|
||||
return;
|
||||
}
|
||||
|
||||
let remoteCmd = remoteCmds.find(remoteCmd => remoteCmd.name == oldName);
|
||||
if (!remoteCmd) {
|
||||
Logger.error(
|
||||
Logs.error.commandActionNotFound.replaceAll('{COMMAND_NAME}', oldName)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.info(
|
||||
Logs.info.commandActionRenaming
|
||||
.replaceAll('{OLD_COMMAND_NAME}', remoteCmd.name)
|
||||
.replaceAll('{NEW_COMMAND_NAME}', newName)
|
||||
);
|
||||
let body: RESTPatchAPIApplicationCommandJSONBody = {
|
||||
name: newName,
|
||||
};
|
||||
await this.rest.patch(Routes.applicationCommand(Config.client.id, remoteCmd.id), {
|
||||
body,
|
||||
});
|
||||
Logger.info(Logs.info.commandActionRenamed);
|
||||
return;
|
||||
}
|
||||
case 'delete': {
|
||||
let name = args[4];
|
||||
if (!name) {
|
||||
Logger.error(Logs.error.commandActionDeleteMissingArg);
|
||||
return;
|
||||
}
|
||||
|
||||
let remoteCmd = remoteCmds.find(remoteCmd => remoteCmd.name == name);
|
||||
if (!remoteCmd) {
|
||||
Logger.error(
|
||||
Logs.error.commandActionNotFound.replaceAll('{COMMAND_NAME}', name)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.info(
|
||||
Logs.info.commandActionDeleting.replaceAll('{COMMAND_NAME}', remoteCmd.name)
|
||||
);
|
||||
await this.rest.delete(Routes.applicationCommand(Config.client.id, remoteCmd.id));
|
||||
Logger.info(Logs.info.commandActionDeleted);
|
||||
return;
|
||||
}
|
||||
case 'clear': {
|
||||
Logger.info(
|
||||
Logs.info.commandActionClearing.replaceAll(
|
||||
'{COMMAND_LIST}',
|
||||
this.formatCommandList(remoteCmds)
|
||||
)
|
||||
);
|
||||
await this.rest.put(Routes.applicationCommands(Config.client.id), { body: [] });
|
||||
Logger.info(Logs.info.commandActionCleared);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private formatCommandList(
|
||||
cmds: RESTPostAPIApplicationCommandsJSONBody[] | APIApplicationCommand[]
|
||||
): string {
|
||||
return cmds.length > 0
|
||||
? cmds.map((cmd: { name: string }) => `'${cmd.name}'`).join(', ')
|
||||
: 'N/A';
|
||||
}
|
||||
}
|
||||
39
src/services/event-data-service.ts
Normal file
39
src/services/event-data-service.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import {
|
||||
Channel,
|
||||
CommandInteractionOptionResolver,
|
||||
Guild,
|
||||
PartialDMChannel,
|
||||
User,
|
||||
} from 'discord.js';
|
||||
|
||||
import { Language } from '../models/enum-helpers/language.js';
|
||||
import { EventData } from '../models/internal-models.js';
|
||||
|
||||
export class EventDataService {
|
||||
public async create(
|
||||
options: {
|
||||
user?: User;
|
||||
channel?: Channel | PartialDMChannel;
|
||||
guild?: Guild;
|
||||
args?: Omit<CommandInteractionOptionResolver, 'getMessage' | 'getFocused'>;
|
||||
} = {}
|
||||
): Promise<EventData> {
|
||||
// TODO: Retrieve any data you want to pass along in events
|
||||
|
||||
// Event language
|
||||
let lang =
|
||||
options.guild?.preferredLocale &&
|
||||
Language.Enabled.includes(options.guild.preferredLocale)
|
||||
? options.guild.preferredLocale
|
||||
: Language.Default;
|
||||
|
||||
// Guild language
|
||||
let langGuild =
|
||||
options.guild?.preferredLocale &&
|
||||
Language.Enabled.includes(options.guild.preferredLocale)
|
||||
? options.guild.preferredLocale
|
||||
: Language.Default;
|
||||
|
||||
return new EventData(lang, langGuild);
|
||||
}
|
||||
}
|
||||
54
src/services/http-service.ts
Normal file
54
src/services/http-service.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import fetch, { Response } from 'node-fetch';
|
||||
import { URL } from 'node:url';
|
||||
|
||||
export class HttpService {
|
||||
public async get(url: string | URL, authorization: string): Promise<Response> {
|
||||
return await fetch(url.toString(), {
|
||||
method: 'get',
|
||||
headers: {
|
||||
Authorization: authorization,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async post(url: string | URL, authorization: string, body?: object): Promise<Response> {
|
||||
return await fetch(url.toString(), {
|
||||
method: 'post',
|
||||
headers: {
|
||||
Authorization: authorization,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
public async put(url: string | URL, authorization: string, body?: object): Promise<Response> {
|
||||
return await fetch(url.toString(), {
|
||||
method: 'put',
|
||||
headers: {
|
||||
Authorization: authorization,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
public async delete(
|
||||
url: string | URL,
|
||||
authorization: string,
|
||||
body?: object
|
||||
): Promise<Response> {
|
||||
return await fetch(url.toString(), {
|
||||
method: 'delete',
|
||||
headers: {
|
||||
Authorization: authorization,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
7
src/services/index.ts
Normal file
7
src/services/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export { CommandRegistrationService } from './command-registration-service.js';
|
||||
export { EventDataService } from './event-data-service.js';
|
||||
export { HttpService } from './http-service.js';
|
||||
export { JobService } from './job-service.js';
|
||||
export { Lang } from './lang.js';
|
||||
export { Logger } from './logger.js';
|
||||
export { MasterApiService } from './master-api-service.js';
|
||||
53
src/services/job-service.ts
Normal file
53
src/services/job-service.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import parser from 'cron-parser';
|
||||
import { DateTime } from 'luxon';
|
||||
import schedule from 'node-schedule';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import { Logger } from './index.js';
|
||||
import { Job } from '../jobs/index.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
let Logs = require('../../lang/logs.json');
|
||||
|
||||
export class JobService {
|
||||
constructor(private jobs: Job[]) {}
|
||||
|
||||
public start(): void {
|
||||
for (let job of this.jobs) {
|
||||
let jobSchedule = job.runOnce
|
||||
? parser
|
||||
.parseExpression(job.schedule, {
|
||||
currentDate: DateTime.now()
|
||||
.plus({ seconds: job.initialDelaySecs })
|
||||
.toJSDate(),
|
||||
})
|
||||
.next()
|
||||
.toDate()
|
||||
: {
|
||||
start: DateTime.now().plus({ seconds: job.initialDelaySecs }).toJSDate(),
|
||||
rule: job.schedule,
|
||||
};
|
||||
|
||||
schedule.scheduleJob(jobSchedule, async () => {
|
||||
try {
|
||||
if (job.log) {
|
||||
Logger.info(Logs.info.jobRun.replaceAll('{JOB}', job.name));
|
||||
}
|
||||
|
||||
await job.run();
|
||||
|
||||
if (job.log) {
|
||||
Logger.info(Logs.info.jobCompleted.replaceAll('{JOB}', job.name));
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(Logs.error.job.replaceAll('{JOB}', job.name), error);
|
||||
}
|
||||
});
|
||||
Logger.info(
|
||||
Logs.info.jobScheduled
|
||||
.replaceAll('{JOB}', job.name)
|
||||
.replaceAll('{SCHEDULE}', job.schedule)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
83
src/services/lang.ts
Normal file
83
src/services/lang.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { EmbedBuilder, Locale, LocalizationMap, resolveColor } from 'discord.js';
|
||||
import { Linguini, TypeMapper, TypeMappers, Utils } from 'linguini';
|
||||
import path, { dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { Language } from '../models/enum-helpers/index.js';
|
||||
|
||||
export class Lang {
|
||||
private static linguini = new Linguini(
|
||||
path.resolve(dirname(fileURLToPath(import.meta.url)), '../../lang'),
|
||||
'lang'
|
||||
);
|
||||
|
||||
public static getEmbed(
|
||||
location: string,
|
||||
langCode: Locale,
|
||||
variables?: { [name: string]: string }
|
||||
): EmbedBuilder {
|
||||
return (
|
||||
this.linguini.get(location, langCode, this.embedTm, variables) ??
|
||||
this.linguini.get(location, Language.Default, this.embedTm, variables)
|
||||
);
|
||||
}
|
||||
|
||||
public static getRegex(location: string, langCode: Locale): RegExp {
|
||||
return (
|
||||
this.linguini.get(location, langCode, TypeMappers.RegExp) ??
|
||||
this.linguini.get(location, Language.Default, TypeMappers.RegExp)
|
||||
);
|
||||
}
|
||||
|
||||
public static getRef(
|
||||
location: string,
|
||||
langCode: Locale,
|
||||
variables?: { [name: string]: string }
|
||||
): string {
|
||||
return (
|
||||
this.linguini.getRef(location, langCode, variables) ??
|
||||
this.linguini.getRef(location, Language.Default, variables)
|
||||
);
|
||||
}
|
||||
|
||||
public static getRefLocalizationMap(
|
||||
location: string,
|
||||
variables?: { [name: string]: string }
|
||||
): LocalizationMap {
|
||||
let obj = {};
|
||||
for (let langCode of Language.Enabled) {
|
||||
obj[langCode] = this.getRef(location, langCode, variables);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
public static getCom(location: string, variables?: { [name: string]: string }): string {
|
||||
return this.linguini.getCom(location, variables);
|
||||
}
|
||||
|
||||
private static embedTm: TypeMapper<EmbedBuilder> = (jsonValue: any) => {
|
||||
return new EmbedBuilder({
|
||||
author: jsonValue.author,
|
||||
title: Utils.join(jsonValue.title, '\n'),
|
||||
url: jsonValue.url,
|
||||
thumbnail: {
|
||||
url: jsonValue.thumbnail,
|
||||
},
|
||||
description: Utils.join(jsonValue.description, '\n'),
|
||||
fields: jsonValue.fields?.map(field => ({
|
||||
name: Utils.join(field.name, '\n'),
|
||||
value: Utils.join(field.value, '\n'),
|
||||
inline: field.inline ? field.inline : false,
|
||||
})),
|
||||
image: {
|
||||
url: jsonValue.image,
|
||||
},
|
||||
footer: {
|
||||
text: Utils.join(jsonValue.footer?.text, '\n'),
|
||||
iconURL: jsonValue.footer?.icon,
|
||||
},
|
||||
timestamp: jsonValue.timestamp ? Date.now() : undefined,
|
||||
color: resolveColor(jsonValue.color ?? Lang.getCom('colors.default')),
|
||||
});
|
||||
};
|
||||
}
|
||||
92
src/services/logger.ts
Normal file
92
src/services/logger.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import { DiscordAPIError } from 'discord.js';
|
||||
import { Response } from 'node-fetch';
|
||||
import { createRequire } from 'node:module';
|
||||
import pino from 'pino';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
let Config = require('../../config/config.json');
|
||||
|
||||
let logger = pino(
|
||||
{
|
||||
formatters: {
|
||||
level: label => {
|
||||
return { level: label };
|
||||
},
|
||||
},
|
||||
},
|
||||
Config.logging.pretty
|
||||
? pino.transport({
|
||||
target: 'pino-pretty',
|
||||
options: {
|
||||
colorize: true,
|
||||
ignore: 'pid,hostname',
|
||||
translateTime: 'yyyy-mm-dd HH:MM:ss.l',
|
||||
},
|
||||
})
|
||||
: undefined
|
||||
);
|
||||
|
||||
export class Logger {
|
||||
private static shardId: number;
|
||||
|
||||
public static info(message: string, obj?: any): void {
|
||||
obj ? logger.info(obj, message) : logger.info(message);
|
||||
}
|
||||
|
||||
public static warn(message: string, obj?: any): void {
|
||||
obj ? logger.warn(obj, message) : logger.warn(message);
|
||||
}
|
||||
|
||||
public static async error(message: string, obj?: any): Promise<void> {
|
||||
// Log just a message if no error object
|
||||
if (!obj) {
|
||||
logger.error(message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise log details about the error
|
||||
if (typeof obj === 'string') {
|
||||
logger
|
||||
.child({
|
||||
message: obj,
|
||||
})
|
||||
.error(message);
|
||||
} else if (obj instanceof Response) {
|
||||
let resText: string;
|
||||
try {
|
||||
resText = await obj.text();
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
logger
|
||||
.child({
|
||||
path: obj.url,
|
||||
statusCode: obj.status,
|
||||
statusName: obj.statusText,
|
||||
headers: obj.headers.raw(),
|
||||
body: resText,
|
||||
})
|
||||
.error(message);
|
||||
} else if (obj instanceof DiscordAPIError) {
|
||||
logger
|
||||
.child({
|
||||
message: obj.message,
|
||||
code: obj.code,
|
||||
statusCode: obj.status,
|
||||
method: obj.method,
|
||||
url: obj.url,
|
||||
stack: obj.stack,
|
||||
})
|
||||
.error(message);
|
||||
} else {
|
||||
logger.error(obj, message);
|
||||
}
|
||||
}
|
||||
|
||||
public static setShardId(shardId: number): void {
|
||||
if (this.shardId !== shardId) {
|
||||
this.shardId = shardId;
|
||||
logger = logger.child({ shardId });
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/services/master-api-service.ts
Normal file
65
src/services/master-api-service.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { createRequire } from 'node:module';
|
||||
import { URL } from 'node:url';
|
||||
|
||||
import { HttpService } from './index.js';
|
||||
import {
|
||||
LoginClusterResponse,
|
||||
RegisterClusterRequest,
|
||||
RegisterClusterResponse,
|
||||
} from '../models/master-api/index.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
let Config = require('../../config/config.json');
|
||||
|
||||
export class MasterApiService {
|
||||
private clusterId: string;
|
||||
|
||||
constructor(private httpService: HttpService) {}
|
||||
|
||||
public async register(): Promise<void> {
|
||||
let reqBody: RegisterClusterRequest = {
|
||||
shardCount: Config.clustering.shardCount,
|
||||
callback: {
|
||||
url: Config.clustering.callbackUrl,
|
||||
token: Config.api.secret,
|
||||
},
|
||||
};
|
||||
|
||||
let res = await this.httpService.post(
|
||||
new URL('/clusters', Config.clustering.masterApi.url),
|
||||
Config.clustering.masterApi.token,
|
||||
reqBody
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw res;
|
||||
}
|
||||
|
||||
let resBody = (await res.json()) as RegisterClusterResponse;
|
||||
this.clusterId = resBody.id;
|
||||
}
|
||||
|
||||
public async login(): Promise<LoginClusterResponse> {
|
||||
let res = await this.httpService.put(
|
||||
new URL(`/clusters/${this.clusterId}/login`, Config.clustering.masterApi.url),
|
||||
Config.clustering.masterApi.token
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw res;
|
||||
}
|
||||
|
||||
return (await res.json()) as LoginClusterResponse;
|
||||
}
|
||||
|
||||
public async ready(): Promise<void> {
|
||||
let res = await this.httpService.put(
|
||||
new URL(`/clusters/${this.clusterId}/ready`, Config.clustering.masterApi.url),
|
||||
Config.clustering.masterApi.token
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw res;
|
||||
}
|
||||
}
|
||||
}
|
||||
143
src/start-bot.ts
Normal file
143
src/start-bot.ts
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import { REST } from '@discordjs/rest';
|
||||
import { Options, Partials } from 'discord.js';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import { Button } from './buttons/index.js';
|
||||
import { DevCommand, HelpCommand, InfoCommand, TestCommand } from './commands/chat/index.js';
|
||||
import {
|
||||
ChatCommandMetadata,
|
||||
Command,
|
||||
MessageCommandMetadata,
|
||||
UserCommandMetadata,
|
||||
} from './commands/index.js';
|
||||
import { ViewDateSent } from './commands/message/index.js';
|
||||
import { ViewDateJoined } from './commands/user/index.js';
|
||||
import {
|
||||
ButtonHandler,
|
||||
CommandHandler,
|
||||
GuildJoinHandler,
|
||||
GuildLeaveHandler,
|
||||
MessageHandler,
|
||||
ReactionHandler,
|
||||
TriggerHandler,
|
||||
} from './events/index.js';
|
||||
import { CustomClient } from './extensions/index.js';
|
||||
import { Job } from './jobs/index.js';
|
||||
import { Bot } from './models/bot.js';
|
||||
import { Reaction } from './reactions/index.js';
|
||||
import {
|
||||
CommandRegistrationService,
|
||||
EventDataService,
|
||||
JobService,
|
||||
Logger,
|
||||
} from './services/index.js';
|
||||
import { Trigger } from './triggers/index.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
let Config = require('../config/config.json');
|
||||
let Logs = require('../lang/logs.json');
|
||||
|
||||
async function start(): Promise<void> {
|
||||
// Services
|
||||
let eventDataService = new EventDataService();
|
||||
|
||||
// Client
|
||||
let client = new CustomClient({
|
||||
intents: Config.client.intents,
|
||||
partials: (Config.client.partials as string[]).map(partial => Partials[partial]),
|
||||
makeCache: Options.cacheWithLimits({
|
||||
// Keep default caching behavior
|
||||
...Options.DefaultMakeCacheSettings,
|
||||
// Override specific options from config
|
||||
...Config.client.caches,
|
||||
}),
|
||||
});
|
||||
|
||||
// Commands
|
||||
let commands: Command[] = [
|
||||
// Chat Commands
|
||||
new DevCommand(),
|
||||
new HelpCommand(),
|
||||
new InfoCommand(),
|
||||
new TestCommand(),
|
||||
|
||||
// Message Context Commands
|
||||
new ViewDateSent(),
|
||||
|
||||
// User Context Commands
|
||||
new ViewDateJoined(),
|
||||
|
||||
// TODO: Add new commands here
|
||||
];
|
||||
|
||||
// Buttons
|
||||
let buttons: Button[] = [
|
||||
// TODO: Add new buttons here
|
||||
];
|
||||
|
||||
// Reactions
|
||||
let reactions: Reaction[] = [
|
||||
// TODO: Add new reactions here
|
||||
];
|
||||
|
||||
// Triggers
|
||||
let triggers: Trigger[] = [
|
||||
// TODO: Add new triggers here
|
||||
];
|
||||
|
||||
// Event handlers
|
||||
let guildJoinHandler = new GuildJoinHandler(eventDataService);
|
||||
let guildLeaveHandler = new GuildLeaveHandler();
|
||||
let commandHandler = new CommandHandler(commands, eventDataService);
|
||||
let buttonHandler = new ButtonHandler(buttons, eventDataService);
|
||||
let triggerHandler = new TriggerHandler(triggers, eventDataService);
|
||||
let messageHandler = new MessageHandler(triggerHandler);
|
||||
let reactionHandler = new ReactionHandler(reactions, eventDataService);
|
||||
|
||||
// Jobs
|
||||
let jobs: Job[] = [
|
||||
// TODO: Add new jobs here
|
||||
];
|
||||
|
||||
// Bot
|
||||
let bot = new Bot(
|
||||
Config.client.token,
|
||||
client,
|
||||
guildJoinHandler,
|
||||
guildLeaveHandler,
|
||||
messageHandler,
|
||||
commandHandler,
|
||||
buttonHandler,
|
||||
reactionHandler,
|
||||
new JobService(jobs)
|
||||
);
|
||||
|
||||
// Register
|
||||
if (process.argv[2] == 'commands') {
|
||||
try {
|
||||
let rest = new REST({ version: '10' }).setToken(Config.client.token);
|
||||
let commandRegistrationService = new CommandRegistrationService(rest);
|
||||
let localCmds = [
|
||||
...Object.values(ChatCommandMetadata).sort((a, b) => (a.name > b.name ? 1 : -1)),
|
||||
...Object.values(MessageCommandMetadata).sort((a, b) => (a.name > b.name ? 1 : -1)),
|
||||
...Object.values(UserCommandMetadata).sort((a, b) => (a.name > b.name ? 1 : -1)),
|
||||
];
|
||||
await commandRegistrationService.process(localCmds, process.argv);
|
||||
} catch (error) {
|
||||
Logger.error(Logs.error.commandAction, error);
|
||||
}
|
||||
// Wait for any final logs to be written.
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
process.exit();
|
||||
}
|
||||
|
||||
await bot.start();
|
||||
}
|
||||
|
||||
process.on('unhandledRejection', (reason, _promise) => {
|
||||
Logger.error(Logs.error.unhandledRejection, reason);
|
||||
});
|
||||
|
||||
start().catch(error => {
|
||||
Logger.error(Logs.error.unspecified, error);
|
||||
});
|
||||
90
src/start-manager.ts
Normal file
90
src/start-manager.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { ShardingManager } from 'discord.js';
|
||||
import { createRequire } from 'node:module';
|
||||
import 'reflect-metadata';
|
||||
|
||||
import { GuildsController, RootController, ShardsController } from './controllers/index.js';
|
||||
import { Job, UpdateServerCountJob } from './jobs/index.js';
|
||||
import { Api } from './models/api.js';
|
||||
import { Manager } from './models/manager.js';
|
||||
import { HttpService, JobService, Logger, MasterApiService } from './services/index.js';
|
||||
import { MathUtils, ShardUtils } from './utils/index.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
let Config = require('../config/config.json');
|
||||
let Debug = require('../config/debug.json');
|
||||
let Logs = require('../lang/logs.json');
|
||||
|
||||
async function start(): Promise<void> {
|
||||
Logger.info(Logs.info.appStarted);
|
||||
|
||||
// Dependencies
|
||||
let httpService = new HttpService();
|
||||
let masterApiService = new MasterApiService(httpService);
|
||||
if (Config.clustering.enabled) {
|
||||
await masterApiService.register();
|
||||
}
|
||||
|
||||
// Sharding
|
||||
let shardList: number[];
|
||||
let totalShards: number;
|
||||
try {
|
||||
if (Config.clustering.enabled) {
|
||||
let resBody = await masterApiService.login();
|
||||
shardList = resBody.shardList;
|
||||
let requiredShards = await ShardUtils.requiredShardCount(Config.client.token);
|
||||
totalShards = Math.max(requiredShards, resBody.totalShards);
|
||||
} else {
|
||||
let recommendedShards = await ShardUtils.recommendedShardCount(
|
||||
Config.client.token,
|
||||
Config.sharding.serversPerShard
|
||||
);
|
||||
shardList = MathUtils.range(0, recommendedShards);
|
||||
totalShards = recommendedShards;
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(Logs.error.retrieveShards, error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (shardList.length === 0) {
|
||||
Logger.warn(Logs.warn.managerNoShards);
|
||||
return;
|
||||
}
|
||||
|
||||
let shardManager = new ShardingManager('dist/start-bot.js', {
|
||||
token: Config.client.token,
|
||||
mode: Debug.override.shardMode.enabled ? Debug.override.shardMode.value : 'process',
|
||||
respawn: true,
|
||||
totalShards,
|
||||
shardList,
|
||||
});
|
||||
|
||||
// Jobs
|
||||
let jobs: Job[] = [
|
||||
Config.clustering.enabled ? undefined : new UpdateServerCountJob(shardManager, httpService),
|
||||
// TODO: Add new jobs here
|
||||
].filter(Boolean);
|
||||
|
||||
let manager = new Manager(shardManager, new JobService(jobs));
|
||||
|
||||
// API
|
||||
let guildsController = new GuildsController(shardManager);
|
||||
let shardsController = new ShardsController(shardManager);
|
||||
let rootController = new RootController();
|
||||
let api = new Api([guildsController, shardsController, rootController]);
|
||||
|
||||
// Start
|
||||
await manager.start();
|
||||
await api.start();
|
||||
if (Config.clustering.enabled) {
|
||||
await masterApiService.ready();
|
||||
}
|
||||
}
|
||||
|
||||
process.on('unhandledRejection', (reason, _promise) => {
|
||||
Logger.error(Logs.error.unhandledRejection, reason);
|
||||
});
|
||||
|
||||
start().catch(error => {
|
||||
Logger.error(Logs.error.unspecified, error);
|
||||
});
|
||||
1
src/triggers/index.ts
Normal file
1
src/triggers/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { Trigger } from './trigger.js';
|
||||
9
src/triggers/trigger.ts
Normal file
9
src/triggers/trigger.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { Message } from 'discord.js';
|
||||
|
||||
import { EventData } from '../models/internal-models.js';
|
||||
|
||||
export interface Trigger {
|
||||
requireGuild: boolean;
|
||||
triggered(msg: Message): boolean;
|
||||
execute(msg: Message, data: EventData): Promise<void>;
|
||||
}
|
||||
247
src/utils/client-utils.ts
Normal file
247
src/utils/client-utils.ts
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
import {
|
||||
ApplicationCommand,
|
||||
Channel,
|
||||
Client,
|
||||
DiscordAPIError,
|
||||
RESTJSONErrorCodes as DiscordApiErrors,
|
||||
Guild,
|
||||
GuildMember,
|
||||
Locale,
|
||||
NewsChannel,
|
||||
Role,
|
||||
StageChannel,
|
||||
TextChannel,
|
||||
User,
|
||||
VoiceChannel,
|
||||
} from 'discord.js';
|
||||
|
||||
import { PermissionUtils, RegexUtils } from './index.js';
|
||||
import { Lang } from '../services/index.js';
|
||||
|
||||
const FETCH_MEMBER_LIMIT = 20;
|
||||
const IGNORED_ERRORS = [
|
||||
DiscordApiErrors.UnknownMessage,
|
||||
DiscordApiErrors.UnknownChannel,
|
||||
DiscordApiErrors.UnknownGuild,
|
||||
DiscordApiErrors.UnknownMember,
|
||||
DiscordApiErrors.UnknownUser,
|
||||
DiscordApiErrors.UnknownInteraction,
|
||||
DiscordApiErrors.MissingAccess,
|
||||
];
|
||||
|
||||
export class ClientUtils {
|
||||
public static async getGuild(client: Client, discordId: string): Promise<Guild> {
|
||||
discordId = RegexUtils.discordId(discordId);
|
||||
if (!discordId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
return await client.guilds.fetch(discordId);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof DiscordAPIError &&
|
||||
typeof error.code == 'number' &&
|
||||
IGNORED_ERRORS.includes(error.code)
|
||||
) {
|
||||
return;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async getChannel(client: Client, discordId: string): Promise<Channel> {
|
||||
discordId = RegexUtils.discordId(discordId);
|
||||
if (!discordId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
return await client.channels.fetch(discordId);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof DiscordAPIError &&
|
||||
typeof error.code == 'number' &&
|
||||
IGNORED_ERRORS.includes(error.code)
|
||||
) {
|
||||
return;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async getUser(client: Client, discordId: string): Promise<User> {
|
||||
discordId = RegexUtils.discordId(discordId);
|
||||
if (!discordId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
return await client.users.fetch(discordId);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof DiscordAPIError &&
|
||||
typeof error.code == 'number' &&
|
||||
IGNORED_ERRORS.includes(error.code)
|
||||
) {
|
||||
return;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async findAppCommand(client: Client, name: string): Promise<ApplicationCommand> {
|
||||
let commands = await client.application.commands.fetch();
|
||||
return commands.find(command => command.name === name);
|
||||
}
|
||||
|
||||
public static async findMember(guild: Guild, input: string): Promise<GuildMember> {
|
||||
try {
|
||||
let discordId = RegexUtils.discordId(input);
|
||||
if (discordId) {
|
||||
return await guild.members.fetch(discordId);
|
||||
}
|
||||
|
||||
let tag = RegexUtils.tag(input);
|
||||
if (tag) {
|
||||
return (
|
||||
await guild.members.fetch({ query: tag.username, limit: FETCH_MEMBER_LIMIT })
|
||||
).find(member => member.user.discriminator === tag.discriminator);
|
||||
}
|
||||
|
||||
return (await guild.members.fetch({ query: input, limit: 1 })).first();
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof DiscordAPIError &&
|
||||
typeof error.code == 'number' &&
|
||||
IGNORED_ERRORS.includes(error.code)
|
||||
) {
|
||||
return;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async findRole(guild: Guild, input: string): Promise<Role> {
|
||||
try {
|
||||
let discordId = RegexUtils.discordId(input);
|
||||
if (discordId) {
|
||||
return await guild.roles.fetch(discordId);
|
||||
}
|
||||
|
||||
let search = input.trim().toLowerCase().replace(/^@/, '');
|
||||
let roles = await guild.roles.fetch();
|
||||
return (
|
||||
roles.find(role => role.name.toLowerCase() === search) ??
|
||||
roles.find(role => role.name.toLowerCase().includes(search))
|
||||
);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof DiscordAPIError &&
|
||||
typeof error.code == 'number' &&
|
||||
IGNORED_ERRORS.includes(error.code)
|
||||
) {
|
||||
return;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async findTextChannel(
|
||||
guild: Guild,
|
||||
input: string
|
||||
): Promise<NewsChannel | TextChannel> {
|
||||
try {
|
||||
let discordId = RegexUtils.discordId(input);
|
||||
if (discordId) {
|
||||
let channel = await guild.channels.fetch(discordId);
|
||||
if (channel instanceof NewsChannel || channel instanceof TextChannel) {
|
||||
return channel;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let search = input.trim().toLowerCase().replace(/^#/, '').replaceAll(' ', '-');
|
||||
let channels = [...(await guild.channels.fetch()).values()]
|
||||
.filter(channel => channel instanceof NewsChannel || channel instanceof TextChannel)
|
||||
.map(channel => channel as NewsChannel | TextChannel);
|
||||
return (
|
||||
channels.find(channel => channel.name.toLowerCase() === search) ??
|
||||
channels.find(channel => channel.name.toLowerCase().includes(search))
|
||||
);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof DiscordAPIError &&
|
||||
typeof error.code == 'number' &&
|
||||
IGNORED_ERRORS.includes(error.code)
|
||||
) {
|
||||
return;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async findVoiceChannel(
|
||||
guild: Guild,
|
||||
input: string
|
||||
): Promise<VoiceChannel | StageChannel> {
|
||||
try {
|
||||
let discordId = RegexUtils.discordId(input);
|
||||
if (discordId) {
|
||||
let channel = await guild.channels.fetch(discordId);
|
||||
if (channel instanceof VoiceChannel || channel instanceof StageChannel) {
|
||||
return channel;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let search = input.trim().toLowerCase().replace(/^#/, '');
|
||||
let channels = [...(await guild.channels.fetch()).values()]
|
||||
.filter(
|
||||
channel => channel instanceof VoiceChannel || channel instanceof StageChannel
|
||||
)
|
||||
.map(channel => channel as VoiceChannel | StageChannel);
|
||||
return (
|
||||
channels.find(channel => channel.name.toLowerCase() === search) ??
|
||||
channels.find(channel => channel.name.toLowerCase().includes(search))
|
||||
);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof DiscordAPIError &&
|
||||
typeof error.code == 'number' &&
|
||||
IGNORED_ERRORS.includes(error.code)
|
||||
) {
|
||||
return;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async findNotifyChannel(
|
||||
guild: Guild,
|
||||
langCode: Locale
|
||||
): Promise<TextChannel | NewsChannel> {
|
||||
// Prefer the system channel
|
||||
let systemChannel = guild.systemChannel;
|
||||
if (systemChannel && PermissionUtils.canSend(systemChannel, true)) {
|
||||
return systemChannel;
|
||||
}
|
||||
|
||||
// Otherwise look for a bot channel
|
||||
return (await guild.channels.fetch()).find(
|
||||
channel =>
|
||||
(channel instanceof TextChannel || channel instanceof NewsChannel) &&
|
||||
PermissionUtils.canSend(channel, true) &&
|
||||
Lang.getRegex('channelRegexes.bot', langCode).test(channel.name)
|
||||
) as TextChannel | NewsChannel;
|
||||
}
|
||||
}
|
||||
73
src/utils/command-utils.ts
Normal file
73
src/utils/command-utils.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import {
|
||||
CommandInteraction,
|
||||
GuildChannel,
|
||||
MessageComponentInteraction,
|
||||
ModalSubmitInteraction,
|
||||
ThreadChannel,
|
||||
} from 'discord.js';
|
||||
|
||||
import { FormatUtils, InteractionUtils } from './index.js';
|
||||
import { Command } from '../commands/index.js';
|
||||
import { Permission } from '../models/enum-helpers/index.js';
|
||||
import { EventData } from '../models/internal-models.js';
|
||||
import { Lang } from '../services/index.js';
|
||||
|
||||
export class CommandUtils {
|
||||
public static findCommand(commands: Command[], commandParts: string[]): Command {
|
||||
let found = [...commands];
|
||||
let closestMatch: Command;
|
||||
for (let [index, commandPart] of commandParts.entries()) {
|
||||
found = found.filter(command => command.names[index] === commandPart);
|
||||
if (found.length === 0) {
|
||||
return closestMatch;
|
||||
}
|
||||
|
||||
if (found.length === 1) {
|
||||
return found[0];
|
||||
}
|
||||
|
||||
let exactMatch = found.find(command => command.names.length === index + 1);
|
||||
if (exactMatch) {
|
||||
closestMatch = exactMatch;
|
||||
}
|
||||
}
|
||||
return closestMatch;
|
||||
}
|
||||
|
||||
public static async runChecks(
|
||||
command: Command,
|
||||
intr: CommandInteraction | MessageComponentInteraction | ModalSubmitInteraction,
|
||||
data: EventData
|
||||
): Promise<boolean> {
|
||||
if (command.cooldown) {
|
||||
let limited = command.cooldown.take(intr.user.id);
|
||||
if (limited) {
|
||||
await InteractionUtils.send(
|
||||
intr,
|
||||
Lang.getEmbed('validationEmbeds.cooldownHit', data.lang, {
|
||||
AMOUNT: command.cooldown.amount.toLocaleString(data.lang),
|
||||
INTERVAL: FormatUtils.duration(command.cooldown.interval, data.lang),
|
||||
})
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(intr.channel instanceof GuildChannel || intr.channel instanceof ThreadChannel) &&
|
||||
!intr.channel.permissionsFor(intr.client.user).has(command.requireClientPerms)
|
||||
) {
|
||||
await InteractionUtils.send(
|
||||
intr,
|
||||
Lang.getEmbed('validationEmbeds.missingClientPerms', data.lang, {
|
||||
PERMISSIONS: command.requireClientPerms
|
||||
.map(perm => `**${Permission.Data[perm].displayName(data.lang)}**`)
|
||||
.join(', '),
|
||||
})
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
57
src/utils/format-utils.ts
Normal file
57
src/utils/format-utils.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { ApplicationCommand, Guild, Locale } from 'discord.js';
|
||||
import { filesize } from 'filesize';
|
||||
import { Duration } from 'luxon';
|
||||
|
||||
export class FormatUtils {
|
||||
public static roleMention(guild: Guild, discordId: string): string {
|
||||
if (discordId === '@here') {
|
||||
return discordId;
|
||||
}
|
||||
|
||||
if (discordId === guild.id) {
|
||||
return '@everyone';
|
||||
}
|
||||
|
||||
return `<@&${discordId}>`;
|
||||
}
|
||||
|
||||
public static channelMention(discordId: string): string {
|
||||
return `<#${discordId}>`;
|
||||
}
|
||||
|
||||
public static userMention(discordId: string): string {
|
||||
return `<@!${discordId}>`;
|
||||
}
|
||||
|
||||
// TODO: Replace with ApplicationCommand#toString() once discord.js #8818 is merged
|
||||
// https://github.com/discordjs/discord.js/pull/8818
|
||||
public static commandMention(command: ApplicationCommand, subParts: string[] = []): string {
|
||||
let name = [command.name, ...subParts].join(' ');
|
||||
return `</${name}:${command.id}>`;
|
||||
}
|
||||
|
||||
public static duration(milliseconds: number, langCode: Locale): string {
|
||||
return Duration.fromObject(
|
||||
Object.fromEntries(
|
||||
Object.entries(
|
||||
Duration.fromMillis(milliseconds, { locale: langCode })
|
||||
.shiftTo(
|
||||
'year',
|
||||
'quarter',
|
||||
'month',
|
||||
'week',
|
||||
'day',
|
||||
'hour',
|
||||
'minute',
|
||||
'second'
|
||||
)
|
||||
.toObject()
|
||||
).filter(([_, value]) => !!value) // Remove units that are 0
|
||||
)
|
||||
).toHuman({ maximumFractionDigits: 0 });
|
||||
}
|
||||
|
||||
public static fileSize(bytes: number): string {
|
||||
return filesize(bytes, { output: 'string', pad: true, round: 2 }) as string;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue