├── .gitignore ├── LICENSE ├── README.md ├── bot.ts ├── commands ├── context │ └── test.ts └── slash │ ├── Owner │ └── Blacklist │ │ ├── add.ts │ │ ├── list.ts │ │ ├── remove.ts │ │ └── show.ts │ ├── ping.ts │ └── test.ts ├── config ├── SlashCommandDirSetup.ts ├── config_sample.ts └── emoji.ts ├── events ├── guildCreate.ts ├── guildDelete.ts ├── interactionCreate.ts ├── messageCreate.ts └── ready.ts ├── extenders └── antiCrash.ts ├── handlers ├── Button-Handlers │ └── ButtonsOwnerBlacklistList.ts ├── Command-Handler │ ├── AutocompleteCommand.ts │ ├── ContextCommand.ts │ └── SlashCommand.ts ├── InteractionBlacklist.ts └── MessageBlacklist.ts ├── index.ts ├── languages ├── en-US.json └── ru.json ├── package.json ├── structures ├── BotClient.ts ├── Cache.ts ├── Database.ts ├── Functions.ts ├── Language.ts ├── Logger.ts └── Sharder.ts ├── tsconfig.json └── utils ├── functions_init.ts ├── init.ts ├── otherTypes.ts └── schema.prisma /.gitignore: -------------------------------------------------------------------------------- 1 | # Runtime data 2 | pids 3 | *.pid 4 | *.seed 5 | *.pid.lock 6 | 7 | # Dependency directories 8 | node_modules 9 | package-lock.json 10 | yarn.lock 11 | 12 | # Optional npm cache directory 13 | .npm 14 | 15 | # Optional eslint cache 16 | .eslintcache 17 | 18 | # Output of 'npm pack' 19 | *.tgz 20 | 21 | # dotenv environment variables file 22 | .env 23 | 24 | # VSCode settings 25 | .vscode 26 | 27 | #Intelli thingy 28 | .idea 29 | 30 | #Config 31 | config/config.ts -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Undefined-Developers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Erry Bot Handler for Discord.js v14 (TypeScript) 2 | 3 | Welcome to the Erry Bot Handler repository! This robust handler is designed to streamline your Discord bot development using the latest features of Discord.js v14 with TypeScript. This handler will be used for Erry bot. 4 | 5 | ## 🚀 Features 6 | 7 | - **TypeScript Ready**: Crafted for the modern TypeScript ecosystem. 8 | - **Up-to-Date Dependencies**: Always running on the latest versions for peak performance. 9 | - **Advanced Sharding**: Implements `discord-hybrid-sharding` and `discord-cross-hosting` for efficient clustering and sharding. 10 | - **Prisma Integration**: Utilizes `prisma` as a powerful database wrapper for PostgreSQL. 11 | - **Comprehensive Tools**: Comes with an integrated database, Redis cache, cross-hosting, and sharding capabilities. 12 | - **User-Friendly Commands**: Jumpstart your bot with pre-generated commands (`npm run ...`) for ease of use. 13 | 14 | ## ✅ Used by: 15 | - Contact me in my [discord server](https://discord.fish/undefined) to be added 16 | 1. **discord.fish Bot** - [Click](https://discord.fish) 17 | 18 | ## ⚙️ Installation & Setup 19 | 20 | 1. **Clone handler**: ```git clone https://github.com/Undefined-Developers/Discord.js-v14-TS-handler handler```. 21 | 2. **Configure**: Rename `config/config_sample.ts` to `config/config.ts` in config folder. 22 | 3. **Customize Configuration**: Fill in the `config.ts` as needed. Don't worry, not all fields are mandatory for initial setup, and defaults work just fine! 23 | 4. **Initialization**: Execute ```npm run init``` and await the magic. 24 | 5. **Launch**: Once you see `SETUP IS DONE` in the console, you're all set! Start handler with ```npm run start```. Discover more commands in `package.json`. 25 | 26 | **Success**: Your bot is now live and ready to engage! 27 | 28 | *P.S. Everything was tested on node v22.12.0 and v20.17.0* 29 | 30 | ## npm run init - Why? 31 | I created this script to simplify and speed up setup, but if your host disallow file writes and/or you can't use scripts, there's manual way: 32 | 33 | 1. Run ```npm i```. This will install all the required packages 34 | 2. Run ```npm i -g pm2```. This will install pm2 so bot will run in background without any consoles open. 35 | 3. Create following entries in .env: 36 | - `DATABASE_URL` and paste database link from config 37 | - `BOT_PROCESS_NAME` and paste botName from config 38 | - `TOKEN` and paste bot token from config 39 | - `AUTH_KEY` and paste bridge_authToken from config 40 | - `REDIS` and paste redis link from config 41 | 4. Paste following as scripts in package.json: 42 | ``` 43 | "scripts": { 44 | "init": "npm install pm2 -g && npm install && npx tsx utils/init.ts && npx prisma db push && echo SETUP IS DONE", 45 | "start": "pm2 start --name 'erry_handler' npx -- tsx index.ts && pm2 save && pm2 log 'erry_handler'", 46 | "restart": "pm2 restart 'erry_handler' && pm2 log 'erry_handler'", 47 | "stop": "pm2 stop 'erry_handler'", 48 | "delete": "pm2 delete 'erry_handler' && pm2 save --force", 49 | "start:cmd": "npx tsx index.js" 50 | }, 51 | ``` 52 | 5. Run ```npx prisma db push```. This will upload schemas to database so it can save data. 53 | 6. Init done! 54 | 55 | ## 🎥 Video Setup Guide (a lil old) 56 | 57 | [![Youtube handler setup](https://img.youtube.com/vi/L6jpBGFcxu0/0.jpg)](https://www.youtube.com/watch?v=L6jpBGFcxu0) -------------------------------------------------------------------------------- /bot.ts: -------------------------------------------------------------------------------- 1 | import { BotClient } from './structures/BotClient'; 2 | 3 | const client = new BotClient(); 4 | 5 | client.on("ErryLoaded", () => { 6 | client.logger.info("Now connecting to discord..."); 7 | client.login(client.config.token) 8 | }) -------------------------------------------------------------------------------- /commands/context/test.ts: -------------------------------------------------------------------------------- 1 | import { ErrySuccessEmbed } from '../../structures/Functions'; 2 | import { ContextCommand, contextTypes } from '../../utils/otherTypes'; 3 | 4 | export default { 5 | name: "test", 6 | type: contextTypes.message, // "message" or "user" only 7 | async execute(client, interaction, es, ls, GuildSettings) { 8 | await interaction.reply({ 9 | embeds:[ 10 | new ErrySuccessEmbed(es) 11 | .setTitle(`IT WORKED!!!!!!`) 12 | .setFooter({text: `Used on message with id: ${interaction.targetId}`}) 13 | ], 14 | flags: [ 15 | 'Ephemeral' 16 | ], 17 | }) 18 | } 19 | } as ContextCommand -------------------------------------------------------------------------------- /commands/slash/Owner/Blacklist/add.ts: -------------------------------------------------------------------------------- 1 | import { ErrySuccessEmbed, ErryWarningEmbed } from '../../../../structures/Functions'; 2 | import { CommandExport, optionTypes } from '../../../../utils/otherTypes'; 3 | 4 | export default { 5 | options: [ 6 | { 7 | type: optionTypes.stringChoices, 8 | name: "type", 9 | required: true, 10 | choices: [ 11 | {name: "User", value: "user"}, 12 | {name: "Guild", value: "guild"} 13 | ], 14 | }, 15 | { 16 | type: optionTypes.string, 17 | name: "id", 18 | required: true 19 | }, 20 | { 21 | type: optionTypes.string, 22 | name: "reason", 23 | required: true 24 | } 25 | ], 26 | async execute(client, interaction, es, ls, GuildSettings) { 27 | await interaction.deferReply() 28 | 29 | const type = interaction.options.getString("type") 30 | const id = interaction.options.getString("id") 31 | const reason = interaction.options.getString("reason") 32 | 33 | if (!type || !id || !reason) 34 | return interaction.editReply({embeds:[ 35 | new ErryWarningEmbed(es) 36 | .setTitle("You need to provide all data") 37 | ]}) 38 | 39 | let data = 40 | type == "user" ? 41 | await client.db.userBlacklist.findUnique({where:{id:id}}) 42 | : 43 | await client.db.guildBlacklist.findUnique({where:{id:id}}) 44 | 45 | if (data) { 46 | return await interaction.editReply({embeds:[ 47 | new ErryWarningEmbed(es) 48 | .setTitle(`This ${type} already been blacklisted with reason:`) 49 | .setDescription(`\`\`\`${data.reason}\`\`\``) 50 | ]}) 51 | } 52 | 53 | type == "user" ? 54 | await client.db.userBlacklist.create({ 55 | data:{ 56 | id: id, 57 | reason: reason 58 | } 59 | }) 60 | : 61 | await client.db.guildBlacklist.create({ 62 | data:{ 63 | id: id, 64 | reason: reason 65 | } 66 | }) 67 | 68 | await interaction.editReply({embeds:[ 69 | new ErrySuccessEmbed(es) 70 | .setTitle(`Added ${type} ${id} to blacklist`) 71 | ]}) 72 | } 73 | } as CommandExport -------------------------------------------------------------------------------- /commands/slash/Owner/Blacklist/list.ts: -------------------------------------------------------------------------------- 1 | import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; 2 | 3 | import { ErrySuccessEmbed, ErryWarningEmbed } from '../../../../structures/Functions'; 4 | import { CommandExport, optionTypes } from '../../../../utils/otherTypes'; 5 | 6 | export default { 7 | options: [ 8 | { 9 | type: optionTypes.stringChoices, 10 | name: "type", 11 | required: true, 12 | choices: [ 13 | {name: "User", value: "user"}, 14 | {name: "Guild", value: "guild"} 15 | ], 16 | } 17 | ], 18 | async execute(client, interaction, es, ls, GuildSettings) { 19 | await interaction.deferReply() 20 | 21 | const type = interaction.options.getString("type") 22 | 23 | if (!type) 24 | return interaction.editReply({embeds:[ 25 | new ErryWarningEmbed(es) 26 | .setTitle("You need to provide all data") 27 | ]}) 28 | 29 | let data = 30 | type == "user" ? 31 | await client.db.userBlacklist.findMany() 32 | : 33 | await client.db.guildBlacklist.findMany() 34 | 35 | if (!data || !data[0]) { 36 | return await interaction.editReply({embeds:[ 37 | new ErryWarningEmbed(es) 38 | .setTitle(`This type (${type}) don't have any records`) 39 | ]}) 40 | } 41 | 42 | const nextButton = new ButtonBuilder() 43 | .setCustomId("owner_blacklist_list:nextButton") 44 | .setLabel("Next") 45 | .setStyle(ButtonStyle.Primary) 46 | 47 | const previousButton = new ButtonBuilder() 48 | .setCustomId("owner_blacklist_list:previousButton") 49 | .setLabel("Prev") 50 | .setStyle(ButtonStyle.Primary) 51 | .setDisabled() 52 | 53 | const chunks = []; 54 | for (let i = 0; i < data.length; i += 5) { 55 | chunks.push(data.slice(i, i + 5)); 56 | } 57 | 58 | const string = chunks[0].map(e => `- \`${e.id}\`: ${e.reason}`).join("\n") 59 | 60 | if (chunks.length == 1) nextButton.setDisabled() 61 | 62 | const actionRow = new ActionRowBuilder().addComponents([previousButton, nextButton]) 63 | 64 | await interaction.editReply({ 65 | embeds:[ 66 | new ErrySuccessEmbed(es, {footer:true}) 67 | .setTitle(`${type} blacklist:`) 68 | .setDescription(`${string}`) 69 | .setFooter({text: `Page #1/${chunks.length}`}) 70 | ], 71 | components: [ 72 | //@ts-ignore 73 | actionRow 74 | ] 75 | }) 76 | } 77 | } as CommandExport -------------------------------------------------------------------------------- /commands/slash/Owner/Blacklist/remove.ts: -------------------------------------------------------------------------------- 1 | import { ErrySuccessEmbed, ErryWarningEmbed } from '../../../../structures/Functions'; 2 | import { CommandExport, optionTypes } from '../../../../utils/otherTypes'; 3 | 4 | export default { 5 | options: [ 6 | { 7 | type: optionTypes.stringChoices, 8 | name: "type", 9 | required: true, 10 | choices: [ 11 | {name: "User", value: "user"}, 12 | {name: "Guild", value: "guild"} 13 | ], 14 | }, 15 | { 16 | type: optionTypes.string, 17 | name: "id", 18 | required: true 19 | } 20 | ], 21 | async execute(client, interaction, es, ls, GuildSettings) { 22 | await interaction.deferReply() 23 | 24 | const type = interaction.options.getString("type") 25 | const id = interaction.options.getString("id") 26 | 27 | if (!type || !id) 28 | return interaction.editReply({embeds:[ 29 | new ErryWarningEmbed(es) 30 | .setTitle("You need to provide all data") 31 | ]}) 32 | 33 | let data = 34 | type == "user" ? 35 | await client.db.userBlacklist.findUnique({where:{id:id}}) 36 | : 37 | await client.db.guildBlacklist.findUnique({where:{id:id}}) 38 | 39 | if (!data) { 40 | return await interaction.editReply({embeds:[ 41 | new ErryWarningEmbed(es) 42 | .setTitle(`This ${type} isn't found in blacklist`) 43 | ]}) 44 | } 45 | 46 | type == "user" ? 47 | await client.db.userBlacklist.delete({where:{id:id}}) 48 | : 49 | await client.db.guildBlacklist.delete({where:{id:id}}) 50 | 51 | await interaction.editReply({embeds:[ 52 | new ErrySuccessEmbed(es) 53 | .setTitle(`Removed ${type} ${id} from blacklist`) 54 | ]}) 55 | } 56 | } as CommandExport -------------------------------------------------------------------------------- /commands/slash/Owner/Blacklist/show.ts: -------------------------------------------------------------------------------- 1 | import { ErrySuccessEmbed, ErryWarningEmbed } from '../../../../structures/Functions'; 2 | import { CommandExport, optionTypes } from '../../../../utils/otherTypes'; 3 | 4 | export default { 5 | options: [ 6 | { 7 | type: optionTypes.stringChoices, 8 | name: "type", 9 | required: true, 10 | choices: [ 11 | {name: "User", value: "user"}, 12 | {name: "Guild", value: "guild"} 13 | ], 14 | }, 15 | { 16 | type: optionTypes.string, 17 | name: "id", 18 | required: true 19 | } 20 | ], 21 | async execute(client, interaction, es, ls, GuildSettings) { 22 | await interaction.deferReply() 23 | 24 | const type = interaction.options.getString("type") 25 | const id = interaction.options.getString("id") 26 | 27 | if (!type || !id) 28 | return interaction.editReply({embeds:[ 29 | new ErryWarningEmbed(es) 30 | .setTitle("You need to provide all data") 31 | ]}) 32 | 33 | let data = 34 | type == "user" ? 35 | await client.db.userBlacklist.findUnique({where:{id:id}}) 36 | : 37 | await client.db.guildBlacklist.findUnique({where:{id:id}}) 38 | 39 | if (!data) { 40 | return await interaction.editReply({embeds:[ 41 | new ErryWarningEmbed(es) 42 | .setTitle(`This ${type} isn't found in blacklist`) 43 | ]}) 44 | } 45 | 46 | await interaction.editReply({embeds:[ 47 | new ErrySuccessEmbed(es) 48 | .setTitle(`Reason for this ${type} ${id} blacklist:`) 49 | .setDescription(`${data.reason}`) 50 | ]}) 51 | } 52 | } as CommandExport -------------------------------------------------------------------------------- /commands/slash/ping.ts: -------------------------------------------------------------------------------- 1 | import { ErrySuccessEmbed } from '../../structures/Functions.ts'; 2 | import {CommandExport, contexts} from '../../utils/otherTypes.ts'; 3 | 4 | export default { 5 | contexts: [contexts.dm, contexts.guild], 6 | async execute(client, interaction, es, ls, GuildSettings) { 7 | await interaction.reply({ 8 | embeds: [ 9 | new ErrySuccessEmbed(es) 10 | .setDescription(client.lang.translate("commands.ping.reply", ls, {ping: `${client.ws.ping}`, dbping: `${await client.db.getPing()}`})) 11 | ] 12 | }) 13 | } 14 | } as CommandExport -------------------------------------------------------------------------------- /commands/slash/test.ts: -------------------------------------------------------------------------------- 1 | import { ErrySuccessEmbed } from '../../structures/Functions'; 2 | import {CommandExport, contexts, optionTypes} from '../../utils/otherTypes'; 3 | 4 | export default { 5 | options: [ 6 | { 7 | type: optionTypes.string, 8 | name: "test", 9 | required: true, 10 | autocomplete: true 11 | } 12 | ], 13 | contexts: [ 14 | contexts.guild, 15 | contexts.dm, // Make command available withing DMs with bot 16 | //contexts.groupDm // Make command available withing group DMs with bot 17 | // P.S. Contexts only works with Slash groups (main group) and just commands like this one. This is I believe limitation of discord.js. At least for now 18 | ], 19 | async execute(client, interaction, es, ls, GuildSettings) { 20 | const option = interaction.options.getString("test") 21 | await interaction.reply({ 22 | embeds:[ 23 | new ErrySuccessEmbed(es) 24 | .setTitle(`IT WORKED!!!!!!`) 25 | .setFooter(client.functions.getFooter(es, `Option text: ${client.functions.getFooter(es, undefined, option)}`)) 26 | ], 27 | flags: [ // New way of sending Ephemeral message. All flags: 'Ephemeral' | 'SuppressEmbeds' | 'SuppressNotifications' 28 | 'Ephemeral' 29 | ] 30 | }) 31 | }, 32 | async autocomplete(client, interaction, es, ls, GuildSettings) { 33 | // You can make database requests and return something dynamic tho 34 | await interaction.respond([ 35 | { 36 | name: 'Option 1', 37 | value: 'option1', 38 | }, 39 | { 40 | name: 'Option 2', 41 | value: 'option2', 42 | } 43 | ]) 44 | } 45 | } as CommandExport -------------------------------------------------------------------------------- /config/SlashCommandDirSetup.ts: -------------------------------------------------------------------------------- 1 | import { PermissionFlagsBits, PermissionsBitField } from 'discord.js'; 2 | 3 | import { dirSetup as dirSetupType, contexts } from '../utils/otherTypes'; 4 | 5 | export const dirSetup = [ 6 | { 7 | Folder: "Info", // Folder Name 8 | name: "info", // Command Name (you'll see in discord) 9 | 10 | // defaultPermissions: new PermissionsBitField([PermissionFlagsBits.Administrator]).bitfield, 11 | // dmPermissions: new PermissionsBitField([PermissionFlagsBits.Administrator]).bitfield, 12 | 13 | /* localizations: [ 14 | { 15 | language: "en-US" 16 | name: "info" 17 | description: "Get Information about Server/User/Bot/..." 18 | } 19 | ]*/ 20 | 21 | description: "Get Information about Server/User/Bot/...", 22 | groups: [ 23 | { 24 | Folder: "Server", 25 | name: "server", 26 | description: "Server specific Informations", 27 | }, 28 | { 29 | Folder: "Bot", 30 | name: "bot", 31 | description: "Bot specific Informations", 32 | } 33 | ] 34 | }, 35 | { 36 | Folder: "Owner", 37 | name: "owner", 38 | 39 | contexts: [ 40 | contexts.guild, 41 | //contexts.dm, // Make group available withing DMs with bot 42 | //contexts.groupDm // Make group available withing group DMs with bot 43 | ], 44 | 45 | defaultPermissions: new PermissionsBitField([PermissionFlagsBits.Administrator]).bitfield, 46 | 47 | description: "DON't TOUCH THIS COMMANDS! pls", 48 | groups: [ 49 | { 50 | Folder: "Blacklist", 51 | name: "blacklist", 52 | description: "Blacklist commands", 53 | } 54 | ] 55 | }, 56 | ] as dirSetupType[] -------------------------------------------------------------------------------- /config/config_sample.ts: -------------------------------------------------------------------------------- 1 | const 2 | bridge_authToken = "auth", // Password for discord-cross-hosting bridge. Input for first init, then you can remove it (writes to .env) 3 | bridge_host = "127.0.0.1", // ip of hosted bridge (localhost if it's hosted on this machine) 4 | bridge_port = 4444, // port of bridge 5 | bridge_create = true, // Create bridge and ignore host and port or use credentials to connect to existing one? 6 | bridge_totalShards = "auto", // How many shards to spawn (Only if bridge_create is true) 7 | bridge_shardsPerCluster = 10, // How many shards should be spawned on 1 cluster 8 | bridge_machines = 1, // How many machines you are running this bot 9 | 10 | token = "", // Bot token. Input for first init, then you can remove it (writes to .env) 11 | 12 | logLevel = { // How much data you want to log to console or webhook 13 | debug: true, // Send all debug data to console 14 | info: true, // Send all info data to console 15 | error: true, // Send all error data to console 16 | success: true, // Send all success data to console 17 | warn: true, // Send all warn data to console 18 | log: true, // Send all log data to console 19 | 20 | webhook: { 21 | guilds: "", // Discord webhook for guild join/leave (not required) 22 | logs: "", // Discord webhook for logs in discord (not required, everything below will not work) 23 | debug: false, // UNSAFE WARNING Send all debug data to webhook - UNRECOMMENDED BECAUSE LOGGER DOESN'T CARE ABOUT RATELIMITS 24 | info: true, // Send all info data to webhook 25 | error: true, // Send all error data to webhook 26 | success: true, // Send all success data to webhook 27 | warn: true, // Send all warn data to webhook 28 | log: true, // Send all log data to webhook 29 | serverlog: true // Send guild join log data to webhook 30 | } 31 | }, 32 | 33 | database = "", // postgresql database connection link. Input for first init, then you can remove it (writes to .env) 34 | 35 | botName = "erry_handler", // Name of PM2 process 36 | 37 | redis = "", // Redis or any redis-based key-value storage connection link (Tested only with redis-server). Input for first init, then you can remove it (writes to .env) 38 | 39 | id = "", // Bot ID 40 | 41 | devCommands = [ // Array with all commands that should upload ONLY to dev guilds 42 | "owner" 43 | ], 44 | devGuilds = [ // Dev guilds 45 | "1008300478146293760" 46 | ], 47 | ownerIDs = [ // Bot owners 48 | "913117505541775420" 49 | ], 50 | 51 | defaultLanguage = "en-US", // default bot language (https://discord.com/developers/docs/reference#locales) 52 | 53 | embed = { // Bot's default embed settings 54 | color: "#25fa6c", // default color 55 | wrongcolor: "#e01e01", // default color while error 56 | warncolor: "#ffa500", // default color while warn 57 | footertext: "Erry The Best", // default footer text 58 | footericon: "" // default image at footer (link to image) 59 | }, 60 | 61 | status = { // Bot statuses 62 | activities: [ // All the activities 63 | { 64 | text: "Undefined Dev.", // Text to show 65 | type: "Listening", // "Listening", "Watching", "Playing"... 66 | }, 67 | { 68 | text: "something", // Text to show 69 | type: "Streaming", // "Listening", "Watching", "Playing"... 70 | url: "https://twitch.tv/*" // url for "Streaming" status 71 | } 72 | ], 73 | status: "online" // "online", "idle", "dnd", "offline" 74 | }; 75 | 76 | const 77 | cooldownCategoriesHigh = [""], 78 | cooldownCommandsHigh = [""], 79 | defaultCooldownMsHigh = 5000, 80 | cooldownCategories = [""], 81 | cooldownCommands = ["test"], 82 | defaultCooldownMs = 400, 83 | maximumCoolDownCommands = { 84 | time: 10000, 85 | amount: 6, 86 | } 87 | 88 | 89 | // HANDLER -- HANDLER -- HANDLER -- HANDLER -- HANDLER -- HANDLER -- HANDLER -- HANDLER -- HANDLER -- HANDLER -- HANDLER 90 | 91 | export const config = { 92 | "bridge_host": bridge_host, 93 | "bridge_port": bridge_port, 94 | "bridge_authToken": process.env.AUTH_KEY || bridge_authToken, 95 | "bridge_create": bridge_create, 96 | "bridge_totalShards": bridge_totalShards, 97 | "bridge_shardsPerCluster": bridge_shardsPerCluster, 98 | "bridge_machines": bridge_machines, 99 | "token": process.env.TOKEN || token, 100 | "logLevel": logLevel, 101 | "database": process.env.DATABASE_URL || database, 102 | "botName": botName, 103 | "redis": process.env.REDIS || redis, 104 | "id": id, 105 | "devCommands": devCommands, 106 | "devGuilds": devGuilds, 107 | "defaultLanguage": defaultLanguage, 108 | "embed": { 109 | "color": embed.color, 110 | "wrongcolor": embed.wrongcolor, 111 | "warncolor": embed.warncolor, 112 | "footertext": embed.footertext, 113 | "footericon": embed.footericon 114 | }, 115 | "status": { 116 | "activities": status.activities, 117 | "status": status.status 118 | }, 119 | "ownerIDs": ownerIDs 120 | } as Config 121 | 122 | export const cooldowns = { 123 | cooldownCategoriesHigh, 124 | cooldownCommandsHigh, 125 | defaultCooldownMsHigh, 126 | cooldownCategories, 127 | cooldownCommands, 128 | defaultCooldownMs, 129 | maximumCoolDownCommands 130 | } as Cooldowns 131 | 132 | interface Cooldowns { 133 | cooldownCategoriesHigh: string[] 134 | cooldownCommandsHigh: string[] 135 | defaultCooldownMsHigh: number 136 | cooldownCategories: string[] 137 | cooldownCommands: string[] 138 | defaultCooldownMs: number 139 | maximumCoolDownCommands: { 140 | time: number 141 | amount: number 142 | } 143 | } 144 | 145 | export interface Config { 146 | "bridge_authToken": string, 147 | "bridge_host": string, 148 | "bridge_port": number, 149 | "bridge_create": boolean, 150 | "bridge_totalShards": "auto" | number, 151 | "bridge_shardsPerCluster": "auto" | number, 152 | "bridge_machines": number, 153 | "token": string, 154 | "logLevel": LogOptions, 155 | "database": string, 156 | "botName": string, 157 | "redis": string, 158 | "devCommands": string[], 159 | "devGuilds": string[], 160 | "defaultLanguage": Locale, 161 | "embed": Embed 162 | "status": Status 163 | "ownerIDs": string[] 164 | } 165 | 166 | export interface Embed { 167 | "guildId"?: string 168 | "color": ColorResolvable 169 | "wrongcolor": ColorResolvable 170 | "warncolor": ColorResolvable 171 | "footertext": string 172 | "footericon": string 173 | } 174 | 175 | export type Status = { 176 | activities: StatusActivities[], 177 | status: PresenceStatusData 178 | } 179 | 180 | export type StatusActivities = { 181 | text: string 182 | type: "Listening"|"Watching"|"Playing"|"Streaming"|"Custom"|"Competing" 183 | url?: string 184 | } 185 | export interface LogOptions { 186 | debug?: boolean, 187 | info?: boolean, 188 | error?: boolean, 189 | success?: boolean, 190 | warn?: boolean, 191 | log?: boolean, 192 | webhook?: webhookOptions 193 | } 194 | 195 | export type webhookOptions = { 196 | guilds?: string 197 | logs?: string 198 | debug?: boolean, 199 | info?: boolean, 200 | error?: boolean, 201 | success?: boolean, 202 | warn?: boolean, 203 | log?: boolean, 204 | serverlog?: boolean 205 | } 206 | 207 | // NEXT TYPES ARE COPIED FROM DISCORD.JS TYPES TO STOP CONFIG THROWING ERRORS WITHOUT MODULES 208 | type ColorResolvable = 209 | | keyof typeof Colors 210 | | 'Random' 211 | | readonly [red: number, green: number, blue: number] 212 | | number 213 | | HexColorString; 214 | declare const Colors: { 215 | Default: 0x000000; 216 | White: 0xffffff; 217 | Aqua: 0x1abc9c; 218 | Green: 0x57f287; 219 | Blue: 0x3498db; 220 | Yellow: 0xfee75c; 221 | Purple: 0x9b59b6; 222 | LuminousVividPink: 0xe91e63; 223 | Fuchsia: 0xeb459e; 224 | Gold: 0xf1c40f; 225 | Orange: 0xe67e22; 226 | Red: 0xed4245; 227 | Grey: 0x95a5a6; 228 | Navy: 0x34495e; 229 | DarkAqua: 0x11806a; 230 | DarkGreen: 0x1f8b4c; 231 | DarkBlue: 0x206694; 232 | DarkPurple: 0x71368a; 233 | DarkVividPink: 0xad1457; 234 | DarkGold: 0xc27c0e; 235 | DarkOrange: 0xa84300; 236 | DarkRed: 0x992d22; 237 | DarkGrey: 0x979c9f; 238 | DarkerGrey: 0x7f8c8d; 239 | LightGrey: 0xbcc0c0; 240 | DarkNavy: 0x2c3e50; 241 | Blurple: 0x5865f2; 242 | Greyple: 0x99aab5; 243 | DarkButNotBlack: 0x2c2f33; 244 | NotQuiteBlack: 0x23272a; 245 | }; 246 | type HexColorString = `#${string}`; 247 | declare enum Locale { 248 | Indonesian = "id", 249 | EnglishUS = "en-US", 250 | EnglishGB = "en-GB", 251 | Bulgarian = "bg", 252 | ChineseCN = "zh-CN", 253 | ChineseTW = "zh-TW", 254 | Croatian = "hr", 255 | Czech = "cs", 256 | Danish = "da", 257 | Dutch = "nl", 258 | Finnish = "fi", 259 | French = "fr", 260 | German = "de", 261 | Greek = "el", 262 | Hindi = "hi", 263 | Hungarian = "hu", 264 | Italian = "it", 265 | Japanese = "ja", 266 | Korean = "ko", 267 | Lithuanian = "lt", 268 | Norwegian = "no", 269 | Polish = "pl", 270 | PortugueseBR = "pt-BR", 271 | Romanian = "ro", 272 | Russian = "ru", 273 | SpanishES = "es-ES", 274 | Swedish = "sv-SE", 275 | Thai = "th", 276 | Turkish = "tr", 277 | Ukrainian = "uk", 278 | Vietnamese = "vi" 279 | } 280 | type PresenceStatusData = ClientPresenceStatus | 'invisible'; 281 | type ClientPresenceStatus = 'online' | 'idle' | 'dnd'; 282 | -------------------------------------------------------------------------------- /config/emoji.ts: -------------------------------------------------------------------------------- 1 | var 2 | join = "", 3 | leave = "<:leaves:1129709317554176100>", 4 | timeanimated = "", 5 | loading = "", 6 | yes = "", 7 | no = "<:no:1129709329797357631>", 8 | arrow = "<:arrow:1129709332141965415>", 9 | spotify = "<:spotify:1129709334708899951>", 10 | soundcloud = "<:SoundCloud:1129709337904939018>", 11 | deezer = "<:deezer:1129709340522192928>", 12 | youtube = "<:YoutubeBig:1129709344192221254>", 13 | list = "📃", 14 | home = "<:home:1129709347136606350>", 15 | skip = "<:skip:1129709349883875388>", 16 | shuffle = "<:shuffle:1129709352685686855>", 17 | pause = "<:pause:1129709355466494043>", 18 | play = "<:play:1129709359073595423>", 19 | joines = "<:joines:1129709361883795467>", 20 | onlinestatic = "<:online:1129709364316491787>", 21 | song = "<:song_loop:1129709366677868634>", 22 | queue = "<:queue_loop:1129709369274155058>", 23 | rewind = "<:rewind:1129709372738633748>", 24 | forward = "<:forward:1129709375490109521>", 25 | replay = "<:replay:1129709379248197754>", 26 | cross = "<:clearqueue:1129709381995483317>", 27 | on = "<:on:1129709383945818213>", 28 | off = "<:off:977868613895741470>", 29 | save = "<:save:1129709390061121677>", 30 | inv = "<:invis:1129709393383010345>", 31 | stop = "<:stop:1129709396222558229>", 32 | erry = "<:erry:1129709401163432056>", 33 | discord = "<:Discord:1129709403759722587>", 34 | bot = "<:Bot_Flag:1129709406490210334>", 35 | server = "", 36 | storm = "", 37 | link = "<:Link:1129709414455185458>", 38 | uptime = "", 39 | online = "", 40 | offline = "", 41 | administration = "<:administration:1129709427268792400>", 42 | speaker = "<:low_volume:1129709431026896926>", 43 | action = "💢", 44 | slash = "<:slash:1129709434852085860>", 45 | users = "👥", 46 | textChannel = "<:Channel:1129709437343518720>", 47 | image = "🖼️", 48 | applemusic = "💢" 49 | 50 | // HANDLER -- HANDLER -- HANDLER -- HANDLER -- HANDLER -- HANDLER -- HANDLER -- HANDLER -- HANDLER -- HANDLER -- HANDLER 51 | 52 | export const emojis = { 53 | "join": join, 54 | "leave": leave, 55 | "timeanimated": timeanimated, 56 | "loading": loading, 57 | "yes": yes, 58 | "no": no, 59 | "arrow": arrow, 60 | "spotify": spotify, 61 | "soundcloud": soundcloud, 62 | "deezer": deezer, 63 | "youtube": youtube, 64 | "list": list, 65 | "home": home, 66 | "skip": skip, 67 | "shuffle": shuffle, 68 | "pause": pause, 69 | "play": play, 70 | "joines": joines, 71 | "onlinestatic": onlinestatic, 72 | "song": song, 73 | "queue": queue, 74 | "rewind": rewind, 75 | "forward": forward, 76 | "replay": replay, 77 | "cross": cross, 78 | "on": on, 79 | "off": off, 80 | "save": save, 81 | "inv": inv, 82 | "stop": stop, 83 | "erry": erry, 84 | "discord": discord, 85 | "bot": bot, 86 | "server": server, 87 | "storm": storm, 88 | "link": link, 89 | "uptime": uptime, 90 | "online": online, 91 | "offline": offline, 92 | "administration": administration, 93 | "speaker": speaker, 94 | "action": action, 95 | "slash": slash, 96 | "users": users, 97 | "textChannel": textChannel, 98 | "image": image, 99 | "applemusic": applemusic 100 | } as Emojis 101 | 102 | export interface Emojis { 103 | [key: string]: string; 104 | "join": string, 105 | "leave": string, 106 | "timeanimated": string, 107 | "loading": string, 108 | "yes": string, 109 | "no": string, 110 | "arrow": string, 111 | "spotify": string, 112 | "soundcloud": string, 113 | "deezer": string, 114 | "youtube": string, 115 | "list": string, 116 | "home": string, 117 | "skip": string, 118 | "shuffle": string, 119 | "pause": string, 120 | "play": string, 121 | "joines": string, 122 | "onlinestatic": string, 123 | "song": string, 124 | "queue": string, 125 | "rewind": string, 126 | "forward": string, 127 | "replay": string, 128 | "cross": string, 129 | "on": string, 130 | "off": string, 131 | "save": string, 132 | "inv": string, 133 | "stop": string, 134 | "erry": string, 135 | "discord": string, 136 | "bot": string, 137 | "server": string, 138 | "storm": string, 139 | "link": string, 140 | "uptime": string, 141 | "online": string, 142 | "offline": string, 143 | "administration": string, 144 | "speaker": string, 145 | "action": string, 146 | "slash": string, 147 | "users": string, 148 | "textChannel": string, 149 | "image": string, 150 | "applemusic": string 151 | } -------------------------------------------------------------------------------- /events/guildCreate.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, Guild } from 'discord.js'; 2 | 3 | import { BotClient } from '../structures/BotClient'; 4 | 5 | export default async (client: BotClient, guild: Guild) => { 6 | if (!guild) return; 7 | if (!guild.available) return; 8 | client.logger.debug(`Joined a new Guild: ${guild.name} (${guild.id}) | Members: ${guild.memberCount} | Current-Average Members/Guild: ${Math.floor(client.guilds.cache.filter((e) => e.memberCount).reduce((a, g) => a + g.memberCount, 0) / client.guilds.cache.size)}`) 9 | await client.db.createGuildDatabase(guild.id) 10 | let theOwner = await guild.fetchOwner() 11 | let embed = new EmbedBuilder() 12 | .setColor(client.config.embed.color) 13 | .setTitle(`${client.emoji.join} Joined a New Server`) 14 | .addFields([ 15 | { name: "Guild Info", value: `>>> \`\`\`${guild.name} (${guild.id})\`\`\``, }, 16 | { name: "Owner Info", value: `>>> \`\`\`${theOwner ? `${theOwner.user.globalName || theOwner.user.username} (${theOwner.id})` : `${theOwner} (${guild.ownerId})`}\`\`\``, }, 17 | { name: "Member Count", value: `>>> \`\`\`${guild.memberCount}\`\`\``, }, 18 | { name: "Servers Bot is in", value: `>>> \`\`\`${client.guilds.cache.size}\`\`\``, }, 19 | { name: "Leave Server:", value: `>>> \`\`\`/owner leaveserver ${guild.id}\`\`\``, }, 20 | ]) 21 | .setThumbnail(guild.iconURL()); 22 | if (client.logger.options.webhook.serverlog && client.logger.guildWebhook) await client.logger.guildWebhook.send({embeds: [embed]}) 23 | } -------------------------------------------------------------------------------- /events/guildDelete.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, Guild } from 'discord.js'; 2 | 3 | import { BotClient } from '../structures/BotClient'; 4 | 5 | export default async (client: BotClient, guild: Guild) => { 6 | if (!guild) return; 7 | if (!guild.available) return; 8 | client.logger.debug(`Left from Guild: ${guild.name} (${guild.id}) | Members: ${guild.memberCount} | Current-Average Members/Guild: ${Math.floor(client.guilds.cache.filter((e) => e.memberCount).reduce((a, g) => a + g.memberCount, 0) / client.guilds.cache.size)}`) 9 | await client.db.removeGuildDatabase(guild.id) 10 | let theOwner 11 | try{ 12 | theOwner = await guild.fetchOwner() 13 | } catch(e) { 14 | theOwner = { 15 | user: { 16 | globalName: "UNKNOWN (Can't load from cache)", 17 | username: "unknown" 18 | }, 19 | id: "UNKNOWN (Can't load from cache)" 20 | } 21 | } 22 | let embed = new EmbedBuilder() 23 | .setColor(client.config.embed.wrongcolor) 24 | .setTitle(`${client.emoji.leave} Left from Server`) 25 | .addFields([ 26 | { name: "Guild Info", value: `>>> \`\`\`${guild.name} (${guild.id})\`\`\``, }, 27 | { name: "Owner Info", value: `>>> \`\`\`${theOwner ? `${theOwner.user.globalName || theOwner.user.username} (${theOwner.id})` : `${theOwner} (${guild.ownerId})`}\`\`\``, }, 28 | { name: "Member Count", value: `>>> \`\`\`${guild.memberCount}\`\`\``, }, 29 | { name: "Servers Bot is in", value: `>>> \`\`\`${client.guilds.cache.size}\`\`\``, }, 30 | ]) 31 | .setThumbnail(guild.iconURL()); 32 | if (client.logger.options.webhook.serverlog && client.logger.guildWebhook) await client.logger.guildWebhook.send({embeds: [embed]}) 33 | } -------------------------------------------------------------------------------- /events/interactionCreate.ts: -------------------------------------------------------------------------------- 1 | import { Interaction, Locale } from 'discord.js'; 2 | 3 | import { Embed, Settings } from '@prisma/client'; 4 | 5 | import { Embed as ConfigEmbed } from '../config/config'; 6 | import { buttonsOwnerBlacklistList } from '../handlers/Button-Handlers/ButtonsOwnerBlacklistList'; 7 | import { autocompleteCommandHandler } from '../handlers/Command-Handler/AutocompleteCommand'; 8 | import { contextCommandHandler } from '../handlers/Command-Handler/ContextCommand'; 9 | import { slashCommandHandler } from '../handlers/Command-Handler/SlashCommand'; 10 | import { interactionBlackListHandler } from '../handlers/InteractionBlacklist'; 11 | import { BotClient } from '../structures/BotClient'; 12 | 13 | export default async (client: BotClient, interaction: Interaction) => { 14 | let GuildSettings: Settings & {embed: Embed | null} = await getGuildSettings(client, interaction.guild?.id) 15 | 16 | let es = GuildSettings?.embed as ConfigEmbed || client.config.embed as ConfigEmbed 17 | let ls = GuildSettings?.language as Locale|undefined || client.config.defaultLanguage 18 | 19 | if (await interactionBlackListHandler(client, interaction, es, ls, GuildSettings)) return; 20 | 21 | interaction.isChatInputCommand() && await slashCommandHandler(client, interaction, es, ls, GuildSettings); 22 | interaction.isContextMenuCommand() && await contextCommandHandler(client, interaction, es, ls, GuildSettings); 23 | interaction.isAutocomplete() && await autocompleteCommandHandler(client, interaction, es, ls, GuildSettings); 24 | interaction.isMessageComponent() && await runAllComponents(); 25 | 26 | async function runAllComponents() { 27 | await buttonsOwnerBlacklistList(client, interaction, es, ls, GuildSettings); 28 | } 29 | } 30 | 31 | async function getGuildSettings(client: BotClient, guildId?: string, num: number = 0): Promise { 32 | if (!guildId || num >= 4) { 33 | return { 34 | ...client.db.InitialSettingsDatabase, 35 | embed: { 36 | ...client.db.InitialEmbedDatabase 37 | } 38 | }; 39 | } 40 | let GuildSettings = await client.db.settings.findUnique({ 41 | where: { 42 | guildId: guildId, 43 | }, 44 | include: { 45 | embed: true, 46 | }, 47 | }); 48 | if (!GuildSettings || !GuildSettings.embed || !GuildSettings.language) { 49 | await client.db.createGuildDatabase(guildId); 50 | GuildSettings = await getGuildSettings(client, guildId, num+1); 51 | } 52 | return GuildSettings; 53 | } 54 | -------------------------------------------------------------------------------- /events/messageCreate.ts: -------------------------------------------------------------------------------- 1 | import { Locale, Message } from 'discord.js'; 2 | 3 | import { Embed } from '../config/config'; 4 | import { messageBlackListHandler } from '../handlers/MessageBlacklist'; 5 | import { BotClient } from '../structures/BotClient'; 6 | 7 | export default async (client: BotClient, message: Message) => { 8 | if(!message.guild) return; 9 | 10 | let GuildSettings = await client.db.settings.findUnique({ 11 | where: { 12 | guildId: message.guild.id, 13 | }, 14 | include: { 15 | embed: true, 16 | }, 17 | }) 18 | let ls: Locale = GuildSettings?.language as Locale || client.config.defaultLanguage 19 | let es: Embed = GuildSettings?.embed as Embed || client.config.embed 20 | if (!GuildSettings || !ls || !es) { 21 | await client.db.createGuildDatabase(message.guild.id) 22 | GuildSettings = await client.db.settings.findUniqueOrThrow({ 23 | where: { 24 | guildId: message.guild?.id, 25 | }, 26 | include: { 27 | embed: true, 28 | }, 29 | }) 30 | ls = GuildSettings?.language as Locale || client.config.defaultLanguage 31 | es = GuildSettings?.embed as Embed || client.config.embed 32 | } 33 | 34 | if (await messageBlackListHandler(client, message, es, ls, GuildSettings)) return; 35 | 36 | // Message Handlers, for example music requets channel (I have no rn) 37 | } -------------------------------------------------------------------------------- /events/ready.ts: -------------------------------------------------------------------------------- 1 | import { BotClient } from '../structures/BotClient'; 2 | 3 | export default async (client: BotClient) => { 4 | try { 5 | client.logger.success(`Discord Bot is ready as ${client.user?.globalName || client.user?.username}`); 6 | await client.functions.statusUpdater(); 7 | setInterval(() => client.functions.statusUpdater(), 10e3) 8 | await client.publishCommands(client.config.devGuilds || undefined); 9 | await client.prepareCommands(); 10 | }catch(e) {client.logger.error(e as Error)} 11 | } -------------------------------------------------------------------------------- /extenders/antiCrash.ts: -------------------------------------------------------------------------------- 1 | import { BotClient } from '../structures/BotClient'; 2 | 3 | export default (client: BotClient) => { 4 | process.on('unhandledRejection', (reason, p) => { 5 | client.logger.error(reason as Error); 6 | }); 7 | process.on("uncaughtException", (err, origin) => { 8 | client.logger.error(err); 9 | }) 10 | process.on('uncaughtExceptionMonitor', (err, origin) => { 11 | client.logger.error(err); 12 | }); 13 | } -------------------------------------------------------------------------------- /handlers/Button-Handlers/ButtonsOwnerBlacklistList.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionRowBuilder, ButtonBuilder, ButtonStyle, Interaction, Locale 3 | } from 'discord.js'; 4 | 5 | import { Settings } from '@prisma/client'; 6 | 7 | import { Embed } from '../../config/config'; 8 | import { BotClient } from '../../structures/BotClient'; 9 | import { ErryErrorEmbed, ErrySuccessEmbed } from '../../structures/Functions'; 10 | 11 | export async function buttonsOwnerBlacklistList(client: BotClient, interaction: Interaction, es: Embed, ls: Locale, GuildSettings: Settings): Promise { 12 | if (!interaction.isMessageComponent() || !interaction.customId.includes("owner_blacklist_list")) return; 13 | 14 | const type = interaction.message.embeds[0].title?.split(" ")[0]; 15 | let page = parseInt(interaction.message.embeds[0].footer?.text.match(/#(.*?)\//)?.[1] || "1"); 16 | 17 | page = (interaction.customId == "owner_blacklist_list:nextButton" ? page+1 : page-1); 18 | 19 | const data = type == "user" ? await client.db.userBlacklist.findMany() : await client.db.guildBlacklist.findMany(); 20 | 21 | if (!data) { 22 | interaction.update({ 23 | embeds: [new ErryErrorEmbed(es).setTitle("Error getting data")] 24 | }); 25 | return; 26 | } 27 | 28 | const chunks = Array(Math.ceil(data.length / 5)).fill(0).map((_, i) => data.slice(i * 5, i * 5 + 5)); 29 | const string = chunks[page-1].map(e => `- \`${e.id}\`: ${e.reason}`).join("\n"); 30 | 31 | const nextButton = new ButtonBuilder() 32 | .setCustomId("owner_blacklist_list:nextButton") 33 | .setLabel("Next") 34 | .setStyle(ButtonStyle.Primary) 35 | .setDisabled(chunks.length == page); 36 | 37 | const previousButton = new ButtonBuilder() 38 | .setCustomId("owner_blacklist_list:previousButton") 39 | .setLabel("Prev") 40 | .setStyle(ButtonStyle.Primary) 41 | .setDisabled(page == 1); 42 | 43 | const actionRow = new ActionRowBuilder().addComponents([previousButton, nextButton]); 44 | 45 | await interaction.update({ 46 | embeds:[ 47 | new ErrySuccessEmbed(es, {footer:true}) 48 | .setTitle(`${type} blacklist:`) 49 | .setDescription(`${string}`) 50 | .setFooter({text: `Page #${page}/${chunks.length}`}) 51 | ], 52 | components: [ 53 | //@ts-ignore 54 | actionRow 55 | ] 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /handlers/Command-Handler/AutocompleteCommand.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AutocompleteInteraction, Locale, PermissionFlagsBits, PermissionsBitField 3 | } from 'discord.js'; 4 | 5 | import { Settings } from '@prisma/client'; 6 | 7 | import { Embed } from '../../config/config'; 8 | import { BotClient } from '../../structures/BotClient'; 9 | import { Command } from '../../utils/otherTypes'; 10 | 11 | export function onlySecondDuration(duration: number): string { 12 | const time = Math.floor(duration / 1000 * 100) / 100; 13 | return `${time} Sec${time !== 1 ? "s" : ""}` 14 | } 15 | 16 | export async function autocompleteCommandHandler(client: BotClient, interaction: AutocompleteInteraction, es: Embed, ls: Locale, GuildSettings: Settings): Promise { 17 | 18 | const slashCmd = client.commands.get(parseSlashCommandKey(interaction)) as Command; 19 | 20 | if(slashCmd) { 21 | try { 22 | if(!(await checkCommand(client, slashCmd, interaction, es, ls))) return; 23 | let commandName = `${interaction.commandName}${interaction.options.getSubcommandGroup(false) ? `_${interaction.options.getSubcommandGroup(false)}` : ``}${interaction.options.getSubcommand(false) ? `_${interaction.options.getSubcommand(false)}` : ``}` 24 | client.logger.debug(`Autocomplete called for /${commandName}\n\t\t(${interaction?.guild?.name ? interaction?.guild?.name : "DMS"} (${interaction?.guild?.id}) by ${interaction.user.globalName || interaction.user.username} (${interaction.user.id}))`) 25 | slashCmd.autocomplete?.(client, interaction, es = client.config.embed, ls = client.config.defaultLanguage, GuildSettings); 26 | } catch (e) { 27 | client.logger.error(e as Error);client.logger.debug(`Error is for guild ${interaction.guild?.id}`) 28 | const content = client.lang.translate("common.error", ls, {command: slashCmd?.name || "???", error: String((e as Error)?.message ?? e).substring(0, 25)}) 29 | if(!interaction.responded) { 30 | await interaction.respond([ 31 | { 32 | name: content, 33 | value: 'error', 34 | } 35 | ]) 36 | } 37 | } 38 | } 39 | } 40 | 41 | export function parseSlashCommandKey(interaction: AutocompleteInteraction): string { 42 | let keys: string[] = ["slashcmd", interaction.commandName]; 43 | if(interaction.options.getSubcommand(false)) { keys.push(`${interaction.options.getSubcommand(false)}`); keys[0] = "subcmd"; } 44 | if(interaction.options.getSubcommandGroup(false)) { keys.splice(1, 0, `${interaction.options.getSubcommandGroup(false)}`); keys[0] = "groupcmd"; } 45 | return keys.join("_"); 46 | } 47 | 48 | export async function checkCommand(client: BotClient, command: Command, ctx: AutocompleteInteraction, es: Embed, ls: Locale, dontCheckCooldown?: boolean) { 49 | if(command.mustPermissions?.length) { 50 | if(ctx.user.id !== ctx.guild?.ownerId && !((ctx.member?.permissions as PermissionsBitField).has(PermissionFlagsBits.Administrator) && command.mustPermissions.some(x => !((ctx.member?.permissions as PermissionsBitField).has(x))))) { 51 | await ctx.respond([ 52 | { 53 | name: client.lang.translate("common.noperms1", ls), 54 | value: "error" 55 | } 56 | ]).catch(() => null); 57 | return false; 58 | } 59 | } 60 | 61 | if(command.allowedPermissions?.length) { 62 | if(ctx.user.id !== ctx.guild?.ownerId && !((ctx.member?.permissions as PermissionsBitField).has(PermissionFlagsBits.Administrator) && command.allowedPermissions.some(x => !((ctx.member?.permissions as PermissionsBitField).has(x))))) { 63 | await ctx.respond([ 64 | { 65 | name: client.lang.translate("common.noperms2", ls), 66 | value: "error" 67 | } 68 | ]).catch(() => null); 69 | return false; 70 | } 71 | } 72 | 73 | return true; 74 | } -------------------------------------------------------------------------------- /handlers/Command-Handler/ContextCommand.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EmbedBuilder, GuildTextBasedChannel, Locale, MessageContextMenuCommandInteraction, 3 | PermissionFlagsBits, PermissionsBitField, UserContextMenuCommandInteraction 4 | } from 'discord.js'; 5 | 6 | import { Settings } from '@prisma/client'; 7 | 8 | import { 9 | MessageFlags, 10 | } from 'discord-api-types/v10'; 11 | 12 | import { cooldowns, Embed } from '../../config/config'; 13 | import { BotClient } from '../../structures/BotClient'; 14 | import { ErryErrorEmbed } from '../../structures/Functions'; 15 | import { ContextCommand } from '../../utils/otherTypes'; 16 | 17 | export function onlySecondDuration(duration: number): string { 18 | const time = Math.floor(duration / 1000 * 100) / 100; 19 | return `${time} Sec${time !== 1 ? "s" : ""}` 20 | } 21 | 22 | export async function contextCommandHandler(client: BotClient, interaction: MessageContextMenuCommandInteraction|UserContextMenuCommandInteraction, es: Embed, ls: Locale, GuildSettings: Settings): Promise { 23 | 24 | const slashCmd = client.commands.get(parseSlashCommandKey(interaction)) as ContextCommand; 25 | 26 | if(slashCmd) { 27 | try { 28 | if(!(await checkCommand(client, slashCmd, interaction, es, ls))) return; 29 | let commandName = 'shortName' in slashCmd && slashCmd.shortName 30 | client.logger.debug(`Used Context /${commandName}\n\t\t(${interaction?.guild?.name ? interaction?.guild?.name : "DMS"} (${interaction?.guild?.id}) by ${interaction.user.globalName || interaction.user.username} (${interaction.user.id}))`) 31 | slashCmd.execute(client, interaction, es = client.config.embed, ls = client.config.defaultLanguage, GuildSettings); 32 | } catch (e) { 33 | client.logger.error(e as Error);client.logger.debug(`Error is for guild ${interaction.guild?.id}`) 34 | const content = client.lang.translate("common.error", ls, {command: slashCmd?.name || "???", error: String((e as Error)?.message ?? e).substring(0, 500)}) 35 | if(interaction.replied) { 36 | await interaction.editReply({ content: content as string }).then(async (msg) => { 37 | setTimeout(() => { 38 | msg.delete() 39 | }, 5000) 40 | }).catch(() => null); 41 | } else { 42 | await interaction.reply({ content: content as string, flags: [MessageFlags.Ephemeral] }).then(async (msg) => { 43 | setTimeout(() => { 44 | msg.delete() 45 | }, 5000) 46 | }).catch(() => { 47 | (interaction.channel as GuildTextBasedChannel)?.send({ content: content as string }).then(async (msg) => { 48 | setTimeout(() => { 49 | msg.delete() 50 | }, 5000) 51 | }).catch(() => null); 52 | }) 53 | } 54 | } 55 | } 56 | } 57 | 58 | export function parseSlashCommandKey(interaction: MessageContextMenuCommandInteraction|UserContextMenuCommandInteraction): string { 59 | let keys: string[] = ["context", interaction.commandName]; 60 | return keys.join("_"); 61 | } 62 | 63 | export async function checkCommand(client: BotClient, command: ContextCommand, ctx: MessageContextMenuCommandInteraction|UserContextMenuCommandInteraction, es: Embed, ls: Locale, dontCheckCooldown?: boolean) { 64 | if(command.mustPermissions?.length) { 65 | if(ctx.user.id !== ctx.guild?.ownerId && !((ctx.member?.permissions as PermissionsBitField).has(PermissionFlagsBits.Administrator) && command.mustPermissions.some(x => !((ctx.member?.permissions as PermissionsBitField).has(x))))) { 66 | await ctx.reply({ 67 | flags: [ 68 | MessageFlags.Ephemeral, 69 | ], 70 | embeds: [ 71 | new EmbedBuilder() 72 | .setColor(es.wrongcolor) 73 | .setTitle(client.lang.translate("common.noperms1", ls)) 74 | //.setDescription(`>>> ${client.functions.translatePermissions(new PermissionsBitField(command.mustPermissions).toArray(), ls).map(x => `\`${x}\``).join(", ")}`) 75 | ] 76 | }).catch(() => null); 77 | return false; 78 | } 79 | } 80 | 81 | if(command.allowedPermissions?.length) { 82 | if(ctx.user.id !== ctx.guild?.ownerId && !((ctx.member?.permissions as PermissionsBitField).has(PermissionFlagsBits.Administrator) && command.allowedPermissions.some(x => !((ctx.member?.permissions as PermissionsBitField).has(x))))) { 83 | await ctx.reply({ 84 | flags: [ 85 | MessageFlags.Ephemeral, 86 | ], 87 | embeds: [ 88 | new EmbedBuilder() 89 | .setColor(es.wrongcolor) 90 | .setTitle(client.lang.translate("common.noperms2", ls)) 91 | //.setDescription(`>>> ${client.functions.translatePermissions(new PermissionsBitField(command.allowedPermissions).toArray(), ls).map(x => `\`${x}\``).join(", ")}`) 92 | ] 93 | }).catch(() => null); 94 | return false; 95 | } 96 | } 97 | 98 | return !(!dontCheckCooldown && (await isOnCooldown(client, command, ctx, es, ls))); 99 | 100 | 101 | } 102 | 103 | export async function isOnCooldown(client: BotClient, command: ContextCommand, ctx: MessageContextMenuCommandInteraction|UserContextMenuCommandInteraction, es: Embed, ls: Locale): Promise { 104 | const [ userId, guildId ] = [ ctx.user.id, ctx.guild?.id ?? "" ]; 105 | 106 | const defaultCooldown = 107 | cooldowns.cooldownCategoriesHigh.includes(command.category || "") || cooldowns.cooldownCommandsHigh.includes(command.name) 108 | ? cooldowns.defaultCooldownMsHigh : 109 | cooldowns.defaultCooldownMs; 110 | 111 | if(command.cooldown?.user ?? defaultCooldown) { 112 | const userCooldowns = new Map(JSON.parse(await client.cache.get(`userCooldown_${userId}`) || "[]")) as Map; 113 | const commandCooldown = userCooldowns.get(command.name) || 0; 114 | if(commandCooldown > Date.now()) { 115 | ctx.reply({ 116 | flags: [ 117 | MessageFlags.Ephemeral, 118 | ], 119 | embeds: [ 120 | new ErryErrorEmbed(es).addFields({name: client.lang.translate("common.cooldown.cmd", ls), value: client.lang.translate("common.cooldown.cmd_", ls, {time: onlySecondDuration(commandCooldown - Date.now())})}) 121 | ], 122 | }).catch(() => null); 123 | return true; 124 | } 125 | (userCooldowns as Map).set(command.name, Date.now()+(command.cooldown?.user||0)) 126 | await client.cache.set(`userCooldown_${userId}`, JSON.stringify(Array.from(userCooldowns.entries()))); 127 | } 128 | if(command.cooldown?.guild && guildId) { 129 | const guildCooldowns = new Map(JSON.parse(await client.cache.get(`guildCooldown_${guildId}`) || "[]")) as Map; 130 | const commandCooldown = guildCooldowns.get(command.name) || 0; 131 | if(commandCooldown > Date.now()) { 132 | ctx.reply({ 133 | flags: [ 134 | MessageFlags.Ephemeral, 135 | ], 136 | embeds: [ 137 | new ErryErrorEmbed(es).addFields({name: client.lang.translate("common.cooldown.guild", ls), value: client.lang.translate("common.cooldown.guild_", ls, {time: onlySecondDuration(commandCooldown - Date.now())})}) 138 | ], 139 | }).catch(() => null); 140 | return true; 141 | } 142 | guildCooldowns.set(command.name, Date.now() + (command.cooldown?.guild ?? defaultCooldown)) 143 | await client.cache.set(`guildCooldown_${guildId}`, JSON.stringify(Array.from(guildCooldowns.entries()))); 144 | } 145 | const globalCooldowns = JSON.parse(await client.cache.get(`globalCooldown_${userId}`) || "[]"); 146 | const allCools = [...(globalCooldowns || []), Date.now()].filter( x => (Date.now() - x) <= cooldowns.maximumCoolDownCommands.time); 147 | await client.cache.set(`globalCooldown_${userId}`, JSON.stringify(allCools)) 148 | if(allCools.length > cooldowns.maximumCoolDownCommands.amount) { 149 | ctx.reply({ 150 | flags: [ 151 | MessageFlags.Ephemeral, 152 | ], 153 | embeds: [ 154 | new ErryErrorEmbed(es).addFields({name: client.lang.translate("common.cooldown.global", ls), value: client.lang.translate("common.cooldown.global_", ls, {time: String(cooldowns.maximumCoolDownCommands.time / 1000), amount: String(cooldowns.maximumCoolDownCommands.amount)})}) 155 | ], 156 | }).catch(() => null); 157 | return true; 158 | } 159 | return false; 160 | } -------------------------------------------------------------------------------- /handlers/Command-Handler/SlashCommand.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChatInputCommandInteraction, CommandInteraction, EmbedBuilder, GuildTextBasedChannel, 3 | Locale, PermissionFlagsBits, PermissionsBitField 4 | } from 'discord.js'; 5 | 6 | import { 7 | MessageFlags, 8 | } from 'discord-api-types/v10'; 9 | 10 | import { Settings } from '@prisma/client'; 11 | 12 | import { cooldowns, Embed } from '../../config/config'; 13 | import { BotClient } from '../../structures/BotClient'; 14 | import { ErryErrorEmbed } from '../../structures/Functions'; 15 | import {Command, ContextCommand} from '../../utils/otherTypes'; 16 | 17 | export function onlySecondDuration(duration: number): string { 18 | const time = Math.floor(duration / 1000 * 100) / 100; 19 | return `${time} Sec${time !== 1 ? "s" : ""}` 20 | } 21 | 22 | export async function slashCommandHandler(client: BotClient, interaction: ChatInputCommandInteraction, es: Embed, ls: Locale, GuildSettings: Settings): Promise { 23 | 24 | const slashCmd = client.commands.get(parseSlashCommandKey(interaction)) as Command; 25 | 26 | if(slashCmd) { 27 | try { 28 | if(!(await checkCommand(client, slashCmd, interaction, es, ls))) return; 29 | let commandName = `${interaction.commandName}${interaction.options.getSubcommandGroup(false) ? `_${interaction.options.getSubcommandGroup(false)}` : ``}${interaction.options.getSubcommand(false) ? `_${interaction.options.getSubcommand(false)}` : ``}` 30 | client.logger.debug(`Used /${commandName}\n\t\t(${interaction?.guild?.name ? interaction?.guild?.name : "DMS"} (${interaction?.guild?.id}) by ${interaction.user.globalName || interaction.user.username} (${interaction.user.id}))`) 31 | slashCmd.execute(client, interaction, es = client.config.embed, ls = client.config.defaultLanguage, GuildSettings); 32 | } catch (e) { 33 | client.logger.error(e as Error);client.logger.debug(`Error is for guild ${interaction.guild?.id}`) 34 | const content = client.lang.translate("common.error", ls, {command: slashCmd?.name || "???", error: String((e as Error)?.message ?? e).substring(0, 500)}) 35 | if(interaction.replied) { 36 | await interaction.editReply({ content: content as string }).then(async (msg) => { 37 | setTimeout(() => { 38 | msg.delete() 39 | }, 5000) 40 | }).catch(() => null); 41 | } else { 42 | await interaction.reply({ content: content as string, flags: [MessageFlags.Ephemeral] }).then(async (msg) => { 43 | setTimeout(() => { 44 | msg.delete() 45 | }, 5000) 46 | }).catch(() => { 47 | (interaction.channel as GuildTextBasedChannel)?.send({ content: content as string }).then(async (msg) => { 48 | setTimeout(() => { 49 | msg.delete() 50 | }, 5000) 51 | }).catch(() => null); 52 | }) 53 | } 54 | } 55 | } 56 | } 57 | 58 | export function parseSlashCommandKey(interaction: ChatInputCommandInteraction): string { 59 | let keys: string[] = ["slashcmd", interaction.commandName]; 60 | if(interaction.options.getSubcommand(false)) { keys.push(`${interaction.options.getSubcommand(false)}`); keys[0] = "subcmd"; } 61 | if(interaction.options.getSubcommandGroup(false)) { keys.splice(1, 0, `${interaction.options.getSubcommandGroup(false)}`); keys[0] = "groupcmd"; } 62 | return keys.join("_"); 63 | } 64 | 65 | export async function checkCommand(client: BotClient, command: Command|ContextCommand, ctx: ChatInputCommandInteraction, es: Embed, ls: Locale, dontCheckCooldown?: boolean): Promise { 66 | if(command.mustPermissions?.length) { 67 | if(ctx.user.id !== ctx.guild?.ownerId && !((ctx.member?.permissions as PermissionsBitField).has(PermissionFlagsBits.Administrator) && command.mustPermissions.some(x => !((ctx.member?.permissions as PermissionsBitField).has(x))))) { 68 | await ctx.reply({ 69 | flags: [ 70 | MessageFlags.Ephemeral, 71 | ], 72 | embeds: [ 73 | new EmbedBuilder() 74 | .setColor(es.wrongcolor) 75 | .setTitle(client.lang.translate("common.noperms1", ls)) 76 | //.setDescription(`>>> ${client.functions.translatePermissions(new PermissionsBitField(command.mustPermissions).toArray(), ls).map(x => `\`${x}\``).join(", ")}`) 77 | ] 78 | }).catch(() => null); 79 | return false; 80 | } 81 | } 82 | 83 | if(command.allowedPermissions?.length) { 84 | if(ctx.user.id !== ctx.guild?.ownerId && !((ctx.member?.permissions as PermissionsBitField).has(PermissionFlagsBits.Administrator) && command.allowedPermissions.some(x => !((ctx.member?.permissions as PermissionsBitField).has(x))))) { 85 | await ctx.reply({ 86 | flags: [ 87 | MessageFlags.Ephemeral, 88 | ], 89 | embeds: [ 90 | new EmbedBuilder() 91 | .setColor(es.wrongcolor) 92 | .setTitle(client.lang.translate("common.noperms2", ls)) 93 | //.setDescription(`>>> ${client.functions.translatePermissions(new PermissionsBitField(command.allowedPermissions).toArray(), ls).map(x => `\`${x}\``).join(", ")}`) 94 | ] 95 | }).catch(() => null); 96 | return false; 97 | } 98 | } 99 | 100 | return !(!dontCheckCooldown && (await isOnCooldown(client, command, ctx, es, ls))); 101 | 102 | 103 | } 104 | 105 | export async function isOnCooldown(client: BotClient, command: Command|ContextCommand, ctx: CommandInteraction, es: Embed, ls: Locale): Promise { 106 | const [ userId, guildId ] = [ ctx.user.id, ctx.guild?.id ?? "" ]; 107 | 108 | const defaultCooldown = 109 | cooldowns.cooldownCategoriesHigh.includes(command.category || "") || cooldowns.cooldownCommandsHigh.includes(command.name) 110 | ? cooldowns.defaultCooldownMsHigh : 111 | cooldowns.defaultCooldownMs; 112 | 113 | console.log(command.cooldown) 114 | 115 | if(command.cooldown?.user ?? defaultCooldown) { 116 | const userCooldowns = new Map(JSON.parse(await client.cache.get(`userCooldown_${userId}`) || "[]")) as Map; 117 | const commandCooldown = userCooldowns.get(command.name) || 0; 118 | if(commandCooldown > Date.now()) { 119 | ctx.reply({ 120 | flags: [ 121 | MessageFlags.Ephemeral, 122 | ], 123 | embeds: [ 124 | new ErryErrorEmbed(es).addFields({name: client.lang.translate("common.cooldown.cmd", ls), value: client.lang.translate("common.cooldown.cmd_", ls, {time: onlySecondDuration(commandCooldown - Date.now())})}) 125 | ], 126 | }).catch(() => null); 127 | return true; 128 | } 129 | (userCooldowns as Map).set(command.name, Date.now()+(command.cooldown?.user||0)) 130 | await client.cache.set(`userCooldown_${userId}`, JSON.stringify(Array.from(userCooldowns.entries()))); 131 | } 132 | if(command.cooldown?.guild && guildId) { 133 | const guildCooldowns = new Map(JSON.parse(await client.cache.get(`guildCooldown_${guildId}`) || "[]")) as Map; 134 | const commandCooldown = guildCooldowns.get(command.name) || 0; 135 | if(commandCooldown > Date.now()) { 136 | ctx.reply({ 137 | flags: [ 138 | MessageFlags.Ephemeral, 139 | ], 140 | embeds: [ 141 | new ErryErrorEmbed(es).addFields({name: client.lang.translate("common.cooldown.guild", ls), value: client.lang.translate("common.cooldown.guild_", ls, {time: onlySecondDuration(commandCooldown - Date.now())})}) 142 | ], 143 | }).catch(() => null); 144 | return true; 145 | } 146 | guildCooldowns.set(command.name, Date.now() + (command.cooldown?.guild ?? defaultCooldown)) 147 | await client.cache.set(`guildCooldown_${guildId}`, JSON.stringify(Array.from(guildCooldowns.entries()))); 148 | } 149 | const globalCooldowns = JSON.parse(await client.cache.get(`globalCooldown_${userId}`) || "[]"); 150 | const allCools = [...(globalCooldowns || []), Date.now()].filter( x => (Date.now() - x) <= cooldowns.maximumCoolDownCommands.time); 151 | await client.cache.set(`globalCooldown_${userId}`, JSON.stringify(allCools)) 152 | if(allCools.length > cooldowns.maximumCoolDownCommands.amount) { 153 | ctx.reply({ 154 | flags: [ 155 | MessageFlags.Ephemeral, 156 | ], 157 | embeds: [ 158 | new ErryErrorEmbed(es).addFields({name: client.lang.translate("common.cooldown.global", ls), value: client.lang.translate("common.cooldown.global_", ls, {time: String(cooldowns.maximumCoolDownCommands.time / 1000), amount: String(cooldowns.maximumCoolDownCommands.amount)})}) 159 | ], 160 | }).catch(() => null); 161 | return false; 162 | } 163 | return false; 164 | } -------------------------------------------------------------------------------- /handlers/InteractionBlacklist.ts: -------------------------------------------------------------------------------- 1 | import { Interaction, Locale } from 'discord.js'; 2 | 3 | import { Settings } from '@prisma/client'; 4 | 5 | import { Embed } from '../config/config'; 6 | import { BotClient } from '../structures/BotClient'; 7 | import { ErryErrorEmbed } from '../structures/Functions'; 8 | 9 | export async function interactionBlackListHandler(client: BotClient, interaction: Interaction, es: Embed, ls: Locale, GuildSettings: Settings): Promise { 10 | const userDB = await client.db.userBlacklist.findUnique({where:{id:interaction.user.id}}) 11 | if (userDB?.reason) { 12 | interaction.isAutocomplete() && await interaction.respond([ 13 | {name: client.lang.translate('common.blacklist.userInteraction', ls), value: "NOT ALLOWED"} 14 | ]); 15 | (interaction.isChatInputCommand() || interaction.isContextMenuCommand()) && await interaction.reply({ 16 | embeds: [ 17 | new ErryErrorEmbed(es) 18 | .setTitle(client.lang.translate('common.blacklist.userInteraction', ls)) 19 | .addFields( 20 | {name: client.lang.translate('common.blacklist.reason', ls), value: `${userDB.reason}`} 21 | ) 22 | ], 23 | ephemeral: true 24 | }) 25 | return true; 26 | } 27 | if (!interaction.guild?.id) return false; 28 | const guildDB = await client.db.guildBlacklist.findUnique({where:{id:interaction.guild.id}}) 29 | if (guildDB?.reason) { 30 | interaction.isAutocomplete() && await interaction.respond([ 31 | {name: client.lang.translate('common.blacklist.guildInteraction', ls), value: "NOT ALLOWED"} 32 | ]); 33 | (interaction.isChatInputCommand() || interaction.isContextMenuCommand()) && await interaction.reply({ 34 | embeds: [ 35 | new ErryErrorEmbed(es) 36 | .setTitle("This guild have been blacklisted") 37 | .addFields( 38 | {name: client.lang.translate('common.blacklist.whatToDo', ls), value: client.lang.translate('common.blacklist.guildInteraction_desc', ls)} 39 | ) 40 | ], 41 | ephemeral: true 42 | }) 43 | return true; 44 | } 45 | return false; 46 | } -------------------------------------------------------------------------------- /handlers/MessageBlacklist.ts: -------------------------------------------------------------------------------- 1 | import { Locale, Message } from 'discord.js'; 2 | 3 | import { Settings } from '@prisma/client'; 4 | 5 | import { Embed } from '../config/config'; 6 | import { BotClient } from '../structures/BotClient'; 7 | 8 | export async function messageBlackListHandler(client: BotClient, message: Message, es: Embed, ls: Locale, GuildSettings: Settings): Promise { 9 | const userDB = await client.db.userBlacklist.findUnique({where:{id:message.author.id}}) 10 | if (userDB?.reason) return true; 11 | if (!message.guild?.id) return false; 12 | const guildDB = await client.db.guildBlacklist.findUnique({where:{id:message.guild.id}}) 13 | return !!guildDB?.reason; 14 | } -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import("dotenv").then(dotenv => { 2 | dotenv.config() 3 | import('./structures/Sharder.ts').then( 4 | module => { 5 | const { ErryClusterManager } = module; 6 | new ErryClusterManager(); 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /languages/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "commands": { 3 | "test": { 4 | "slashLocalizations": { 5 | "name": "test", 6 | "description": "Test description for command", 7 | "options": { 8 | "1": { 9 | "name": "test", 10 | "description": "What you think?" 11 | } 12 | } 13 | } 14 | }, 15 | "ping": { 16 | "slashLocalizations": { 17 | "name": "ping", 18 | "description": "Test description for ping command" 19 | }, 20 | "reply": "There is `{{ping}}`ms between me and discord\nThere is `{{dbping}}`ms between me and my database" 21 | }, 22 | "owner_blacklist_add": { 23 | "slashLocalizations": { 24 | "name": "add", 25 | "description": "Add user or guild to bot's blacklist", 26 | "options": { 27 | "1": { 28 | "name": "type", 29 | "description": "User or Guild?" 30 | }, 31 | "2": { 32 | "name": "id", 33 | "description": "Id of user or guild to add" 34 | }, 35 | "3": { 36 | "name": "reason", 37 | "description": "Why?" 38 | } 39 | } 40 | } 41 | }, 42 | "owner_blacklist_show": { 43 | "slashLocalizations": { 44 | "name": "show", 45 | "description": "Show user or guild in bot's blacklist", 46 | "options": { 47 | "1": { 48 | "name": "type", 49 | "description": "User or Guild?" 50 | }, 51 | "2": { 52 | "name": "id", 53 | "description": "Id of user or guild to reveal reason" 54 | } 55 | } 56 | } 57 | }, 58 | "owner_blacklist_remove": { 59 | "slashLocalizations": { 60 | "name": "remove", 61 | "description": "Remove user or guild from bot's blacklist", 62 | "options": { 63 | "1": { 64 | "name": "type", 65 | "description": "User or Guild?" 66 | }, 67 | "2": { 68 | "name": "id", 69 | "description": "Id of user or guild to reveal reason" 70 | } 71 | } 72 | } 73 | }, 74 | "owner_blacklist_list": { 75 | "slashLocalizations": { 76 | "name": "list", 77 | "description": "List users or guilds in bot's blacklist", 78 | "options": { 79 | "1": { 80 | "name": "type", 81 | "description": "User or Guild?" 82 | } 83 | } 84 | } 85 | } 86 | }, 87 | "common": { 88 | "blacklist": { 89 | "reason": "Reason", 90 | "whatToDo": "What to do?", 91 | "userInteraction": "You've been blacklisted. Join support for assistance", 92 | "guildInteraction": "This guild have been blacklisted. If you're the owner, join support for assistance", 93 | "guildInteraction_desc": "If you're the owner, join support for assistance. \nIf you're member, maybe this is good to notify owner about this." 94 | }, 95 | "error": "Error while executing `{{command}}`, try again or contact support! ```\n{{error}}\n```", 96 | "noperms1": "{{Emoji_no}} You need __all__ those Permissions:", 97 | "noperms2": "{{Emoji_no}} You need one of those Permissions:", 98 | "cooldown": { 99 | "cmd": "{{Emoji_no}}Wait... Command cooldown", 100 | "guild": "{{Emoji_no}}Wait... Command cooldown on this guild", 101 | "global": "{{Emoji_no}}Wait... STOP PLEASE", 102 | "cmd_": "> {{Emoji_arrow}}You can use this Command in `{{time}}`", 103 | "guild_": "> {{Emoji_arrow}}This Guild can use this Command in `{{time}}`", 104 | "global_": "> {{Emoji_arrow}}You only can use `{{amount}}` Commands per `{{time}}` Seconds" 105 | }, 106 | "owner": { 107 | "notowner": "{{Emoji_no}} You're not the owner of this bot or you haven't permission to do that", 108 | "notownerdesc": "{{Emoji_arrow}} To use theese commandsd you need to be 1 of this users: \n>>> {{info}}" 109 | }, 110 | "metrics": { 111 | "seconds": "Seconds", 112 | "secondsShort": "s", 113 | "minutes": "Minutes", 114 | "minutesShort": "m", 115 | "hours": "Hours", 116 | "hoursShort": "h", 117 | "days": "Days", 118 | "daysShort": "d", 119 | "miliseconds": "Miliseconds", 120 | "milisecondsShort": "ms" 121 | }, 122 | "permissions": { 123 | "AddReactions": "Add Reactions", 124 | "Administrator": "Administrator", 125 | "AttachFiles": "Attach Files", 126 | "BanMembers": "Ban Members", 127 | "ChangeNickname": "Change Nickname", 128 | "Connect": "Connect", 129 | "CreateInstantInvite": "Create Invites", 130 | "CreatePrivateThreads": "Create Private Threads", 131 | "CreatePublicThreads": "Create Public Threads", 132 | "DeafenMembers": "Deafen Members", 133 | "EmbedLinks": "Embed Links", 134 | "KickMembers": "Kick Members", 135 | "ManageChannels": "Manage Channels", 136 | "ManageEmojisAndStickers": "Manage Emojis And Stickers (Manage Guild Expressions)", 137 | "ManageEvents": "Manage Events", 138 | "ManageGuild": "Manage Guild", 139 | "ManageGuildExpressions": "Manage Guild Expressions (Manage Emojis And Stickers)", 140 | "ManageMessages": "Manage Messages", 141 | "ManageNicknames": "Manage Nicknames", 142 | "ManageRoles": "Manage Roles", 143 | "ManageThreads": "Manage Threads", 144 | "ManageWebhooks": "Manage Webhooks", 145 | "MentionEveryone": "Mention Everyone", 146 | "ModerateMembers": "Moderate Members", 147 | "MoveMembers": "Move Members", 148 | "MuteMembers": "Mute Members", 149 | "PrioritySpeaker": "Priority Speaker", 150 | "ReadMessageHistory": "Read Messages History", 151 | "RequestToSpeak": "Request To Speak", 152 | "SendMessages": "Send Messages", 153 | "SendMessagesInThreads": "Send Messages In Threads", 154 | "SendTTSMessages": "Send TTS Messages", 155 | "SendVoiceMessages": "Send Voice Messages", 156 | "Speak": "Speak", 157 | "Stream": "Stream", 158 | "UseApplicationCommands": "Use Application Commands", 159 | "UseEmbeddedActivities": "Use Embedded Activities", 160 | "UseExternalEmojis": "Use External Emojis", 161 | "UseExternalSounds": "Use External Sounds", 162 | "UseExternalStickers": "Use External Stickers", 163 | "UseSoundboard": "Use Soundboard", 164 | "UseVAD": "Use Voice Activity Detection", 165 | "ViewAuditLog": "View Audit Log", 166 | "ViewChannel": "View Channel", 167 | "ViewCreatorMonetizationAnalytics": "View Creator Monetization Analytics", 168 | "ViewGuildInsights": "View Guild Insights" 169 | } 170 | } 171 | } -------------------------------------------------------------------------------- /languages/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "commands": { 3 | "ping": { 4 | "slashLocalizations": { 5 | "name": "пинг", 6 | "description": "Тестовое описание для пинг команды" 7 | }, 8 | "reply": "There is `{{ping}}`ms between me and discord\nThere is `{{dbping}}`ms between me and my database" 9 | }, 10 | "test": { 11 | "slashLocalizations": { 12 | "name": "тест", 13 | "description": "Пробное описание", 14 | "options": { 15 | "1": { 16 | "name": "тест", 17 | "description": "Ну как?" 18 | } 19 | } 20 | } 21 | } 22 | }, 23 | "common": { 24 | "blacklist": { 25 | "reason": "Причина", 26 | "whatToDo": "Что делать?", 27 | "userInteraction": "Вы были добавлены в черный список. Обратитесь в службу поддержки для получения помощи.", 28 | "guildInteraction": "Этот сервер был добавлен в черный список. Если вы владелец, обратитесь в службу поддержки для получения помощи.", 29 | "guildInteraction_desc": "Если вы владелец, обратитесь в службу поддержки для получения помощи. \nЕсли вы участник, возможно, будет полезно уведомить владельца об этом." 30 | }, 31 | "error": "Ошибка при выполнении `{{command}}`, попробуйте снова или свяжитесь с поддержкой! ```\n{{error}}\n```", 32 | "noperms1": "{{Emoji_no}} Вам нужны __все__ следующие права:", 33 | "noperms2": "{{Emoji_no}} Вам нужно хотя бы одно из следующих прав:", 34 | "cooldown": { 35 | "cmd": "{{Emoji_no}} Подождите... Перезарядка команды", 36 | "guild": "{{Emoji_no}} Подождите... Перезарядка команды на этом сервере", 37 | "global": "{{Emoji_no}} Подождите... ПОЖАЛУЙСТА, СТОП!", 38 | "cmd_": "> {{Emoji_arrow}} Вы сможете использовать эту команду через `{{time}}`", 39 | "guild_": "> {{Emoji_arrow}} Этот сервер сможет использовать эту команду через `{{time}}`", 40 | "global_": "> {{Emoji_arrow}} Вы можете использовать `{{amount}}` команд каждые `{{time}}` секунд" 41 | }, 42 | "owner": { 43 | "notowner": "{{Emoji_no}} Вы не владелец этого бота или у вас нет разрешения на выполнение этого действия", 44 | "notownerdesc": "{{Emoji_arrow}} Для использования этих команд вы должны быть одним из следующих пользователей: \n>>> {{info}}" 45 | }, 46 | "metrics": { 47 | "seconds": "Секунды", 48 | "secondsShort": "с", 49 | "minutes": "Минуты", 50 | "minutesShort": "м", 51 | "hours": "Часы", 52 | "hoursShort": "ч", 53 | "days": "Дни", 54 | "daysShort": "д", 55 | "miliseconds": "Миллисекунды", 56 | "milisecondsShort": "мс" 57 | }, 58 | "permissions": { 59 | "AddReactions": "Добавление реакций", 60 | "Administrator": "Администратор", 61 | "AttachFiles": "Прикрепление файлов", 62 | "BanMembers": "Бан пользователей", 63 | "ChangeNickname": "Изменение никнейма", 64 | "Connect": "Подключение", 65 | "CreateInstantInvite": "Создание приглашений", 66 | "CreatePrivateThreads": "Создание приватных веток", 67 | "CreatePublicThreads": "Создание публичных веток", 68 | "DeafenMembers": "Отключение звука для пользователей", 69 | "EmbedLinks": "Встраивание ссылок", 70 | "KickMembers": "Исключение пользователей", 71 | "ManageChannels": "Управление каналами", 72 | "ManageEmojisAndStickers": "Управление эмодзи и стикерами (Управление выражениями сервера)", 73 | "ManageEvents": "Управление событиями", 74 | "ManageGuild": "Управление сервером", 75 | "ManageGuildExpressions": "Управление выражениями сервера (Управление эмодзи и стикерами)", 76 | "ManageMessages": "Управление сообщениями", 77 | "ManageNicknames": "Управление никнеймами", 78 | "ManageRoles": "Управление ролями", 79 | "ManageThreads": "Управление ветками", 80 | "ManageWebhooks": "Управление вебхуками", 81 | "MentionEveryone": "Упоминание всех", 82 | "ModerateMembers": "Модерация пользователей", 83 | "MoveMembers": "Перемещение пользователей", 84 | "MuteMembers": "Отключение микрофона для пользователей", 85 | "PrioritySpeaker": "Приоритетный оратор", 86 | "ReadMessageHistory": "Чтение истории сообщений", 87 | "RequestToSpeak": "Запрос на выступление", 88 | "SendMessages": "Отправка сообщений", 89 | "SendMessagesInThreads": "Отправка сообщений в ветках", 90 | "SendTTSMessages": "Отправка голосовых сообщений TTS", 91 | "SendVoiceMessages": "Отправка голосовых сообщений", 92 | "Speak": "Говорить", 93 | "Stream": "Трансляция", 94 | "UseApplicationCommands": "Использование команд приложения", 95 | "UseEmbeddedActivities": "Использование встроенных активностей", 96 | "UseExternalEmojis": "Использование внешних эмодзи", 97 | "UseExternalSounds": "Использование внешних звуков", 98 | "UseExternalStickers": "Использование внешних стикеров", 99 | "UseSoundboard": "Использование звуковой панели", 100 | "UseVAD": "Использование распознавания голосовой активности", 101 | "ViewAuditLog": "Просмотр журнала аудита", 102 | "ViewChannel": "Просмотр канала", 103 | "ViewCreatorMonetizationAnalytics": "Просмотр аналитики монетизации автора", 104 | "ViewGuildInsights": "Просмотр аналитики сервера" 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discord.js-v14-ts-handler", 3 | "version": "0.1.3", 4 | "description": "Powerful discord.js v14 handler", 5 | "main": "index.ts", 6 | "scripts": { 7 | "init": "npm install pm2 -g && npm install && npx tsx utils/init.ts && npx prisma db push && echo SETUP IS DONE" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Undefined-Developers/Discord.js-v14-TS-handler.git" 12 | }, 13 | "keywords": [ 14 | "Discord", 15 | "Handler", 16 | "Discordjs", 17 | "erry", 18 | "v14" 19 | ], 20 | "author": "Rocky", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/Undefined-Developers/Discord.js-v14-TS-handler/issues" 24 | }, 25 | "homepage": "https://github.com/Undefined-Developers/Discord.js-v14-TS-handler#readme", 26 | "devDependencies": { 27 | "typescript": "^5.7.3" 28 | }, 29 | "dependencies": { 30 | "@prisma/client": "^6.3.1", 31 | "@types/node": "^22.13.4", 32 | "chalk": "^5.4.1", 33 | "discord-cross-hosting": "^2.3.8", 34 | "discord-hybrid-sharding": "^2.2.4", 35 | "discord.js": "^14.18.0", 36 | "dotenv": "^16.4.7", 37 | "envfile": "^7.1.0", 38 | "moment": "^2.30.1", 39 | "prisma": "^6.3.1", 40 | "redis": "^4.7.0", 41 | "typescript-collections": "^1.3.3" 42 | }, 43 | "prisma": { 44 | "schema": "./utils/schema.prisma" 45 | } 46 | } -------------------------------------------------------------------------------- /structures/BotClient.ts: -------------------------------------------------------------------------------- 1 | import { Shard } from 'discord-cross-hosting'; 2 | import { ClusterClient, DjsDiscordClient, getInfo } from 'discord-hybrid-sharding'; 3 | import { 4 | ActivityType, ApplicationCommand, ApplicationCommandType, Client, ClientOptions, Collection, 5 | ContextMenuCommandBuilder, GatewayIntentBits, GuildResolvable, Partials, PresenceStatusData, 6 | PresenceUpdateStatus, RESTPostAPIChatInputApplicationCommandsJSONBody, 7 | RESTPostAPIContextMenuApplicationCommandsJSONBody, ShardClientUtil, SlashCommandBuilder, 8 | SlashCommandSubcommandBuilder 9 | } from 'discord.js'; 10 | import { promises } from 'fs'; 11 | import { resolve } from 'path'; 12 | import { pathToFileURL } from 'url'; 13 | 14 | import { Config, config } from '../config/config'; 15 | import { Emojis, emojis } from '../config/emoji'; 16 | import { dirSetup } from '../config/SlashCommandDirSetup'; 17 | import { 18 | BotCounters, Command, CommandOptionChannel, commandOptionChoiceNumber, 19 | commandOptionChoiceString, CommandOptionNumberChoices, CommandOptionStringChoices, 20 | ContextCommand, optionTypes 21 | } from '../utils/otherTypes'; 22 | import { ErryCacheManager } from './Cache'; 23 | import { ErryDatabase } from './Database'; 24 | import { ErryFunctions } from './Functions'; 25 | import { ErryLanguage } from './Language'; 26 | import { Logger } from './Logger'; 27 | 28 | export class BotClient extends Client { 29 | public config: Config 30 | public logger: Logger 31 | public cluster: ClusterClient 32 | public commands: Collection 33 | public eventPaths: Collection 34 | public cache: ErryCacheManager 35 | public functions: ErryFunctions 36 | public db: ErryDatabase 37 | public allCommands: (RESTPostAPIContextMenuApplicationCommandsJSONBody | RESTPostAPIChatInputApplicationCommandsJSONBody)[] 38 | public fetchedApplication: ApplicationCommand<{guild: GuildResolvable;}>[] 39 | public machine?: Shard 40 | public lang: ErryLanguage 41 | public emoji: Emojis 42 | constructor(options?: ClientOptions) { 43 | super({ 44 | ...getDefaultClientOptions(), 45 | ...options 46 | }); 47 | this.config = config 48 | this.cluster = new ClusterClient(this); 49 | this.machine = new Shard(this.cluster); 50 | this.logger = new Logger({ prefix: " Erry ", ...this.config.logLevel }); 51 | this.commands = new Collection(); 52 | this.eventPaths = new Collection(); 53 | this.allCommands = []; 54 | this.fetchedApplication = []; 55 | this.functions = new ErryFunctions(this); 56 | this.cache = new ErryCacheManager() 57 | this.db = new ErryDatabase(this.cache) 58 | this.lang = new ErryLanguage() 59 | this.emoji = emojis 60 | this.init(); 61 | } 62 | public async init() { 63 | console.log(`${"-=".repeat(40)}-`); 64 | this.logger.info(`Loading Cache`); 65 | await this.cache.init(); 66 | 67 | console.log(`${"-=".repeat(40)}-`); 68 | this.logger.info(`Loading Database`); 69 | await this.db.init(); 70 | 71 | console.log(`${"-=".repeat(40)}-`); 72 | this.logger.info(`Loading Languages`); 73 | await this.lang.init(); 74 | 75 | console.log(`${"-=".repeat(40)}-`); 76 | this.logger.info(`Loading Events`); 77 | await this.loadEvents(); 78 | 79 | console.log(`${"-=".repeat(40)}-`); 80 | this.logger.info(`Loading Commands`); 81 | await this.loadCommands(); 82 | 83 | console.log(`${"-=".repeat(40)}-`); 84 | this.logger.info(`Loading ContextMenu`); 85 | await this.loadContextMenu(); 86 | 87 | console.log(`${"-=".repeat(40)}-`); 88 | this.logger.info(`Loading Extenders`); 89 | await this.loadExtenders(); 90 | 91 | return this.emit("ErryLoaded", this); 92 | } 93 | public get counters() { 94 | return { 95 | guilds: this.guilds.cache.size, 96 | members: this.guilds.cache.map(x => x.memberCount).reduce((a,b) => a+b,0), 97 | clusterId: this.cluster.id, 98 | shardIds: this.cluster.shardList, 99 | ping: this.ws.ping, 100 | uptime: this.uptime, 101 | } as BotCounters 102 | } 103 | public async loadExtenders() { 104 | try { 105 | const paths = await walks(`${process.cwd()}/extenders`); 106 | await Promise.all( 107 | paths.map(async (path) => { 108 | const extender = await import(globalFilePath(resolve(path))).then(x => x.default) 109 | const name = resolve(path).includes("\\") ? resolve(path).split("\\").reverse()[0] : resolve(path).split("/").reverse()[0]; 110 | this.logger.debug(`✅ Extender Loaded: ${name.replace(".ts", "")}`); 111 | return extender(this); 112 | }) 113 | ); 114 | } catch (e) { 115 | this.logger.error(e as Error); 116 | } 117 | return true; 118 | } 119 | public async loadEvents() { 120 | try { 121 | this.eventPaths.clear(); 122 | const paths = await walks(`${process.cwd()}/events`); 123 | if (paths.length == 0) return; 124 | await Promise.all( 125 | paths.map(async (path) => { 126 | const event = await import(globalFilePath(resolve(path))).then(x => x.default) 127 | const splitted = resolve(path).includes("\\") ? resolve(path).split("\\") : resolve(path).split("/") 128 | const eventName = splitted.reverse()[0].replace(".ts", ""); 129 | this.eventPaths.set(eventName, { eventName, path: resolve(path) }); 130 | this.logger.debug(`✅ Event Loaded: ${eventName}`); 131 | return this.on(eventName, event.bind(null, this)); 132 | }) 133 | ); 134 | } catch (e) { 135 | this.logger.error(e as Error); 136 | } 137 | return true; 138 | } 139 | public async loadCommands(path = "/commands/slash") { 140 | try { 141 | this.allCommands = []; 142 | this.commands.clear(); 143 | const basePath = `${process.cwd()}${path}`; 144 | const dirs = await promises.readdir(basePath); 145 | const dirStats = await Promise.all(dirs.map(dir => promises.lstat(`${basePath}/${dir}`).catch(() => null))); 146 | 147 | for (let i = 0; i < dirs.length; i++) { 148 | const dir = dirs[i]; 149 | const stat = dirStats[i]; 150 | if (!dir.endsWith(".ts") && stat?.isDirectory?.()) { 151 | const thisDirSetup = dirSetup.find(x => x.Folder.toLowerCase() === dir.toLowerCase()); 152 | if (!thisDirSetup) { 153 | this.logger.stringError(`Could not find the DirSetup for ${dir}`); 154 | continue; 155 | } 156 | 157 | const subSlash = new SlashCommandBuilder() 158 | .setName(String(thisDirSetup.name).toLowerCase()) 159 | .setDescription(String(thisDirSetup.description)) 160 | .setContexts(thisDirSetup.contexts || [0]) 161 | 162 | if (thisDirSetup.defaultPermissions) { 163 | subSlash.setDefaultMemberPermissions(thisDirSetup.defaultPermissions); 164 | } 165 | 166 | if (thisDirSetup.dmPermissions) { 167 | subSlash.setDefaultMemberPermissions(thisDirSetup.dmPermissions); 168 | } 169 | 170 | if (thisDirSetup.localizations?.length) { 171 | for (const localization of thisDirSetup.localizations) { 172 | if (localization.name) subSlash.setNameLocalization(localization.language, localization.name); 173 | if (localization.description) subSlash.setDescriptionLocalization(localization.language, localization.description); 174 | } 175 | } 176 | 177 | const dirPath = `${basePath}/${dir}`; 178 | const slashCommands = await promises.readdir(dirPath); 179 | const commandStats = await Promise.all(slashCommands.map(file => promises.lstat(`${dirPath}/${file}`).catch(e => this.logger.error(e)))); 180 | 181 | for (let j = 0; j < slashCommands.length; j++) { 182 | const file = slashCommands[j]; 183 | const commandStat = commandStats[j]; 184 | 185 | const curPath = `${dirPath}/${file}`; 186 | if (commandStat?.isDirectory?.()) { 187 | const groupPath = curPath; 188 | const groupDirSetup = thisDirSetup.groups?.find(x => x.Folder.toLowerCase() == file.toLowerCase()) 189 | if (!groupDirSetup) { 190 | this.logger.stringError(`Could not find the groupDirSetup for ${dir}/${file}`); 191 | continue; 192 | } 193 | const slashCommands = await promises.readdir(groupPath).then(x => x.filter(v => v.endsWith(".ts"))); 194 | if (slashCommands?.length) { 195 | let commands: {[key: string]: Command} = {} 196 | for (let sFile of slashCommands) { 197 | const groupCurPath = `${groupPath}/${sFile}`; 198 | commands[sFile] = await import(globalFilePath(groupCurPath)).then(x => x.default); 199 | } 200 | subSlash.addSubcommandGroup(Group => { 201 | Group.setName(groupDirSetup.name.toLowerCase()).setDescription(groupDirSetup.description || "Temp_Desc"); 202 | if(groupDirSetup.localizations?.length) { 203 | for(const localization of groupDirSetup.localizations) { 204 | if(localization.name) Group.setNameLocalization(localization.language, localization.name); 205 | if(localization.description) Group.setDescriptionLocalization(localization.language, localization.description); 206 | } 207 | } 208 | for (let sFile of slashCommands) { 209 | const command = commands[sFile]; 210 | if (!command.name) { 211 | try { 212 | command.name = this.lang.getSlashCommandName(String(thisDirSetup.name).toLowerCase() + "_" + String(groupDirSetup.name).toLowerCase() + "_" + sFile.split(".ts").join("")) 213 | } catch (e) { 214 | this.logger.stringError(`${e}`); 215 | continue; 216 | } 217 | } 218 | if (!command.description) { 219 | try { 220 | command.description = this.lang.getSlashCommandDescription(String(thisDirSetup.name).toLowerCase() + "_" + String(groupDirSetup.name).toLowerCase() + "_" + sFile.split(".ts").join("")) 221 | } catch (e) { 222 | command.description = "Temp_Desc" 223 | this.logger.warn(`There is no description for ${String(thisDirSetup.name).toLowerCase() + "_" + String(groupDirSetup.name).toLowerCase() + "_" + sFile.split(".ts").join("")} slash command in ${config.defaultLanguage} language file`) 224 | } 225 | } 226 | if (!command.localizations) { 227 | try { 228 | command.localizations = this.lang.getSlashCommandLocalizations(String(thisDirSetup.name).toLowerCase() + "_" + String(groupDirSetup.name).toLowerCase() + "_" + sFile.split(".ts").join("")) 229 | } catch (e) { 230 | // nothing cause who need localizations if there is 1 language? 231 | } 232 | } 233 | Group.addSubcommand(Slash => { 234 | Slash.setName(command.name).setDescription(command.description); 235 | if(command.localizations?.length) { 236 | for(const localization of command.localizations) { 237 | if(!localization.language) continue; 238 | if(localization.name) Slash.setNameLocalization(localization.language, localization.name); 239 | if(localization.description) Slash.setDescriptionLocalization(localization.language, localization.description); 240 | } 241 | } 242 | this.buildCommandOptions(command, Slash, String(thisDirSetup.name).toLowerCase() + "_" + String(groupDirSetup.name).toLowerCase() + "_" + command.name) 243 | return Slash; 244 | }); 245 | command.commandId = this.fetchedApplication?.find?.((c) => c?.name == subSlash.name)?.permissions?.commandId ?? "commandId"; 246 | command.slashCommandKey = `/${subSlash.name} ${Group.name} ${command.name}` 247 | command.mention = `<${command.slashCommandKey}:${command.commandId}>` 248 | this.commands.set("groupcmd_" + String(groupDirSetup.name).toLowerCase() + "_" + String(thisDirSetup.name).toLowerCase() + "_" + command.name, command) 249 | this.logger.debug(`✅ Group Command Loaded: /${thisDirSetup.name} ${groupDirSetup.name} ${command.name}`); 250 | } 251 | return Group; 252 | }); 253 | } 254 | } else { 255 | const command = await import(globalFilePath(curPath)).then(x => x.default); 256 | if (!command.name) { 257 | try { 258 | command.name = this.lang.getSlashCommandName(String(thisDirSetup.name).toLowerCase() + "_" + file.split(".ts").join("")) 259 | } catch (e) { 260 | this.logger.stringError(`${e}`); 261 | continue; 262 | } 263 | } 264 | if (!command.description) { 265 | try { 266 | command.description = this.lang.getSlashCommandDescription(String(thisDirSetup.name).toLowerCase() + "_" + file.split(".ts").join("")) 267 | } catch (e) { 268 | command.description = "Temp_Desc" 269 | this.logger.warn(`There is no description for ${String(thisDirSetup.name).toLowerCase() + "_" + file.split(".ts").join("")} slash command in ${config.defaultLanguage} language file`) 270 | } 271 | } 272 | if (!command.localizations) { 273 | try { 274 | command.localizations = this.lang.getSlashCommandLocalizations(String(thisDirSetup.name).toLowerCase() + "_" + file.split(".ts").join("")) 275 | } catch (e) { 276 | // nothing cause who need localizations if there is 1 language? 277 | } 278 | } 279 | subSlash.addSubcommand(Slash => { 280 | Slash.setName(command.name as string).setDescription(command.description as string) 281 | if(command.localizations?.length) { 282 | for(const localization of command.localizations) { 283 | if(!localization.language) continue; 284 | if(localization.name) Slash.setNameLocalization(localization.language, localization.name); 285 | if(localization.description) Slash.setDescriptionLocalization(localization.language, localization.description); 286 | } 287 | } 288 | this.buildCommandOptions(command, Slash, String(thisDirSetup.name).toLowerCase() + "_" + command.name) 289 | return Slash; 290 | }); 291 | command.commandId = this?.fetchedApplication?.find?.((c) => c?.name == subSlash.name)?.permissions?.commandId ?? "commandId"; 292 | command.slashCommandKey = `/${subSlash.name} ${command.name}` 293 | command.mention = `<${command.slashCommandKey}:${command.commandId}>` 294 | this.commands.set("subcmd_" + String(thisDirSetup.name).toLowerCase() + "_" + command.name, command) 295 | this.logger.debug(`✅ ⠀⠀Sub Command Loaded: /${thisDirSetup.name} ${command.name}`); 296 | } 297 | } 298 | 299 | this.allCommands.push(subSlash.toJSON()); 300 | } else { 301 | const curPath = `${basePath}/${dir}`; 302 | const command = await import(globalFilePath(curPath)).then(x => x.default); 303 | if (!command.name) { 304 | try { 305 | command.name = this.lang.getSlashCommandName(dir.split(".ts").join("")) 306 | } catch (e) { 307 | this.logger.stringError(`${e}`); 308 | continue; 309 | } 310 | } 311 | if (!command.description) { 312 | try { 313 | command.description = this.lang.getSlashCommandDescription(dir.split(".ts").join("")) 314 | } catch (e) { 315 | command.description = "Temp_Desc" 316 | this.logger.warn(`There is no description for ${dir.split(".ts").join("")} slash command in ${config.defaultLanguage} language file`) 317 | } 318 | } 319 | if (!command.localizations) { 320 | try { 321 | command.localizations = this.lang.getSlashCommandLocalizations(dir.split(".ts").join("")) 322 | } catch (e) { 323 | // nothing cause who need localizations if there is 1 language? 324 | } 325 | } 326 | const Slash = new SlashCommandBuilder().setName(command.name as string).setDescription(command.description as string).setContexts(command.contexts || [0]); 327 | if(command.defaultPermissions) { 328 | Slash.setDefaultMemberPermissions(command.defaultPermissions); 329 | } 330 | if(command.dmPermissions) { 331 | Slash.setDefaultMemberPermissions(command.dmPermissions); 332 | } 333 | if(command.localizations?.length) { 334 | for(const localization of command.localizations) { 335 | if(!localization.language) continue; 336 | if(localization.name) Slash.setNameLocalization(localization.language, localization.name); 337 | if(localization.description) Slash.setDescriptionLocalization(localization.language, localization.description); 338 | } 339 | } 340 | this.buildCommandOptions(command, Slash, command.name); 341 | command.commandId = this?.fetchedApplication?.find?.((c) => c?.name == command.name)?.permissions?.commandId ?? "commandId"; 342 | command.slashCommandKey = `/${command.name}` 343 | command.mention = `<${command.slashCommandKey}:${command.commandId}>` 344 | this.commands.set("slashcmd_" + command.name, command) 345 | this.logger.debug(`✅ Slash Command Loaded: /${command.name}`); 346 | this.allCommands.push(Slash.toJSON()); 347 | } 348 | } 349 | } catch (e) { 350 | this.logger.error(e as Error); 351 | } 352 | 353 | return true; 354 | } 355 | public async loadContextMenu(path = "/commands/context") { 356 | try { 357 | const basePath = `${process.cwd()}${path}`; 358 | const dirs = await promises.readdir(basePath); 359 | const commands = await Promise.all(dirs.map(dir => import(globalFilePath(`${basePath}/${dir}`)).then(x => x.default))); 360 | 361 | for (let i = 0; i < dirs.length; i++) { 362 | const dir = dirs[i]; 363 | const command = commands[i] as ContextCommand; 364 | 365 | if (!command.name) { 366 | this.logger.stringError(`${basePath}/${dir} not containing a Command-Name`); 367 | continue; 368 | } 369 | 370 | const Slash = new ContextMenuCommandBuilder() 371 | .setName(command.name) 372 | .setType((ApplicationCommandType[command.type])); 373 | 374 | if (command.localizations) Slash.setNameLocalizations(command.localizations); 375 | if (command.defaultPermissions) Slash.setDefaultMemberPermissions(command.defaultPermissions); 376 | if (command.dmPermissions) Slash.setDefaultMemberPermissions(command.dmPermissions); 377 | 378 | command.commandId = this?.fetchedApplication?.find?.((c) => c?.name == command.name)?.permissions?.commandId ?? "commandId"; 379 | command.slashCommandKey = `/${command.name}`; 380 | command.mention = `<${command.slashCommandKey}:${command.commandId}>`; 381 | command.shortName = dir.split(".ts").join(""); 382 | 383 | this.commands.set("context_" + command.name, command); 384 | this.logger.debug(`✅ Context Command Loaded: /${command.name}`); 385 | this.allCommands.push(Slash.toJSON()); 386 | } 387 | } catch (e) { 388 | this.logger.error(e as Error); 389 | } 390 | 391 | return true; 392 | } 393 | public async prepareCommands() { 394 | const allSlashs = await this.application?.commands.fetch(undefined) 395 | .then(x => [...x.values()]) 396 | .catch(console.warn) 397 | || (this.application?.commands.cache.values() ? [...this.application?.commands.cache.values()] : []) 398 | || []; 399 | if(allSlashs?.length) { 400 | this.fetchedApplication = allSlashs; 401 | for(const [key, value] of [...this.commands.entries()]) { 402 | if(!value.slashCommandKey) continue; 403 | const Base = value.slashCommandKey.split(" ")[0].replace("/", ""); 404 | value.commandId = allSlashs.find(c => c.name === Base)?.permissions?.commandId || "0"; 405 | value.mention = value.mention?.replace("commandId", value.commandId); 406 | this.commands.set(key, value) 407 | } 408 | this.logger.debug(`✅ Set Command Mentions of: ${allSlashs?.length} Commands`); 409 | } 410 | return true; 411 | } 412 | public async publishCommands(guildIds?: string[], del?: boolean) { 413 | if (del) { 414 | for (let guild of this.guilds.cache.values()) { 415 | guild.commands.set([]).catch(e => {this.logger.error(e);this.logger.debug(`recent error were for guild ${guild.name} || ${guild.id}`);}); 416 | } 417 | this.logger.debug(`Deleted all commands!`) 418 | } 419 | if(this.cluster.id !== 0) return; 420 | if(!guildIds) { 421 | await this.application?.commands.set(this.allCommands).then(() => { 422 | this.logger.debug(`SLASH-CMDS | Set ${this.commands.size} slashCommands!`) 423 | }).catch(e => {this.logger.error(e);}); 424 | return true; 425 | } 426 | await this.application?.commands.set(this.allCommands.filter((c) => !this.config.devCommands?.includes?.(c.name))).then(() => { 427 | this.logger.debug(`SLASH-CMDS | Set ${this.commands.size} global slashCommands!`) 428 | }).catch(e => {this.logger.error(e);}); 429 | for (let guildId of guildIds) { 430 | const shardId = ShardClientUtil.shardIdForGuildId(guildId, getInfo().TOTAL_SHARDS) 431 | if(!this.cluster.shardList.includes(shardId)) return this.logger.warn("CANT UPDATE SLASH COMMANDS - WRONG CLUSTER"); 432 | const guild = this.guilds.cache.get(guildId); 433 | if(!guild) return this.logger.stringError(`could not find the guild \`${guildId}\` for updating slash commands`) 434 | guild.commands.set(this.allCommands.filter((c) => this.config.devCommands?.includes?.(c.name))).catch(e => {this.logger.error(e);}); 435 | } 436 | this.logger.debug(`SLASH-CMDS | Set ${this.commands.size} guild slashCommands!`) 437 | return true; 438 | } 439 | private buildOption(op: any, option: any) { 440 | op.setName(option.name.toLowerCase()) 441 | .setDescription(option.description || "TEMP_DESC") 442 | .setRequired(!!option.required); 443 | 444 | if (option.localizations?.length) { 445 | for (const localization of option.localizations) { 446 | if (!localization.language) continue; 447 | if (localization.name) op.setNameLocalization(localization.language, localization.name); 448 | if (localization.description) op.setDescriptionLocalization(localization.language, localization.description); 449 | } 450 | } 451 | return op; 452 | } 453 | private buildCommandOptions(command: Command, Slash: SlashCommandSubcommandBuilder|SlashCommandBuilder, path: string) { 454 | if (command.options?.length) { 455 | for (let option of command.options) { 456 | if (!option.name) { 457 | try { 458 | option.name = this.lang.getSlashCommandOptionName(path, command.options.indexOf(option)+1) 459 | } catch (e) { 460 | this.logger.stringError(`[LOADER] ${command.name} - getSlashCommandOptionName: ${e}`); 461 | continue; 462 | } 463 | } 464 | if (!option.description) { 465 | try { 466 | option.description = this.lang.getSlashCommandOptionDescription(path, command.options.indexOf(option)+1) 467 | } catch (e) { 468 | this.logger.stringError(`[LOADER] ${command.name} - getSlashCommandOptionDescription: ${e}`); 469 | continue; 470 | } 471 | } 472 | if (option.name && !option.localizations) { 473 | try { 474 | option.localizations = this.lang.getSlashCommandOptionLocalizations(path, command.options.indexOf(option)+1) 475 | } catch (e) { 476 | this.logger.stringError(`[LOADER] ${command.name} - getSlashCommandOptionLocalizations: ${e}`); 477 | continue; 478 | } 479 | } 480 | const type = option.type.toLowerCase(); 481 | if (type === optionTypes.attachment) { 482 | Slash.addAttachmentOption(op => this.buildOption(op, option)); 483 | } else if (type === optionTypes.channel) { 484 | Slash.addChannelOption(op => { 485 | op = this.buildOption(op, option); 486 | option = option as CommandOptionChannel 487 | if (option.channelTypes) op.addChannelTypes(...option.channelTypes); 488 | return op; 489 | }); 490 | } else if (type === optionTypes.number || type === optionTypes.numberChoices) { 491 | Slash.addNumberOption(op => { 492 | op = this.buildOption(op, option); 493 | option = option as CommandOptionNumberChoices 494 | op.setAutocomplete(!!option.autocomplete); 495 | if (option.max) op.setMaxValue(option.max); 496 | if (option.min) op.setMinValue(option.min); 497 | if (type === optionTypes.numberChoices && option.choices) { 498 | const numberChoices = option.choices.filter((choice): choice is commandOptionChoiceNumber => typeof choice.value === 'number'); 499 | op.setChoices(...numberChoices); 500 | } 501 | return op; 502 | }); 503 | } else if (type === optionTypes.role) { 504 | Slash.addRoleOption(op => this.buildOption(op, option)); 505 | } else if (type === optionTypes.string || type === optionTypes.stringChoices) { 506 | Slash.addStringOption(op => { 507 | op = this.buildOption(op, option); 508 | option = option as CommandOptionStringChoices 509 | op.setAutocomplete(!!option.autocomplete); 510 | if (option.max) op.setMaxLength(option.max); 511 | if (option.min) op.setMinLength(option.min); 512 | if (type === optionTypes.stringChoices && option.choices) { 513 | const stringChoices = option.choices.filter((choice): choice is commandOptionChoiceString => typeof choice.value === 'string'); 514 | op.setChoices(...stringChoices); 515 | } 516 | return op; 517 | }); 518 | } else if (type === optionTypes.user) { 519 | Slash.addUserOption(op => this.buildOption(op, option)); 520 | } 521 | } 522 | } 523 | return true; 524 | } 525 | } 526 | 527 | export function getDefaultClientOptions() { 528 | return { 529 | shards: getInfo().SHARD_LIST, 530 | shardCount: getInfo().TOTAL_SHARDS, 531 | 532 | partials: [ 533 | Partials.Channel, 534 | Partials.Message, 535 | Partials.GuildMember, 536 | Partials.ThreadMember, 537 | Partials.Reaction, 538 | Partials.User, 539 | Partials.GuildScheduledEvent, 540 | ], 541 | intents: [ 542 | GatewayIntentBits.Guilds, 543 | GatewayIntentBits.GuildMembers, 544 | GatewayIntentBits.GuildModeration, 545 | GatewayIntentBits.GuildExpressions, 546 | GatewayIntentBits.GuildIntegrations, 547 | GatewayIntentBits.GuildWebhooks, 548 | GatewayIntentBits.GuildInvites, 549 | GatewayIntentBits.GuildVoiceStates, 550 | //GatewayIntentBits.GuildPresences, 551 | GatewayIntentBits.GuildMessages, 552 | GatewayIntentBits.GuildMessageReactions, 553 | //GatewayIntentBits.GuildMessageTyping, 554 | GatewayIntentBits.DirectMessages, 555 | GatewayIntentBits.DirectMessageReactions, 556 | //GatewayIntentBits.DirectMessageTyping, 557 | GatewayIntentBits.MessageContent, 558 | GatewayIntentBits.AutoModerationExecution, 559 | GatewayIntentBits.AutoModerationConfiguration 560 | ], 561 | presence: { 562 | activities: [ 563 | { 564 | name: `Booting up`, type: ActivityType.Playing 565 | } 566 | ], 567 | status: PresenceUpdateStatus.DoNotDisturb as PresenceStatusData 568 | }, 569 | failIfNotExists: false, 570 | allowedMentions: { 571 | parse: [], 572 | users: [], 573 | roles: [], 574 | repliedUser: false, 575 | } 576 | }; 577 | } 578 | 579 | export const globalFilePath = (path: string): string => pathToFileURL(path)?.href || path; 580 | 581 | async function walks(path: string, recursive: boolean = true): Promise { 582 | let files: string[] = []; 583 | const items = await promises.readdir(path, { withFileTypes: true }); 584 | for (const item of items) { 585 | if (item.isDirectory() && recursive) { 586 | files = [ ...files, ...(await walks(`${path}/${item.name}`)) ]; 587 | } else if(item.isFile()) { 588 | files.push(`${path}/${item.name}`); 589 | } 590 | } 591 | return files; 592 | } -------------------------------------------------------------------------------- /structures/Cache.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from 'redis'; 2 | import { PriorityQueue } from 'typescript-collections'; 3 | 4 | import { config, Config } from '../config/config'; 5 | import { Logger } from './Logger'; 6 | 7 | export class ErryCacheManager { 8 | private localCache: Map = new Map(); 9 | private TTL = 60e3; 10 | private config: Config = config; 11 | private Client = createClient({ url: this.config.redis }); 12 | private subClient = createClient({ url: this.config.redis }); // Separate client for subscribing 13 | private logger = new Logger({ prefix: " Erry Cache ", ...this.config.logLevel }); 14 | private cacheQueue: PriorityQueue<{ key: string, expiry: number }>; 15 | private invalidationTimeout: NodeJS.Timeout | null = null; 16 | 17 | constructor() { 18 | this.Client.on('error', (err) => this.logger.error(err)); 19 | this.subClient.on('error', (err) => this.logger.error(err)); 20 | this.cacheQueue = new PriorityQueue<{ key: string, expiry: number }>((a, b) => b.expiry - a.expiry); 21 | } 22 | 23 | public async init(): Promise { 24 | await this.Client.connect(); 25 | await this.subClient.connect(); 26 | await this.subClient.subscribe('cache-updates', (message) => { 27 | const { action, key } = JSON.parse(message); 28 | if (action === 'delete') { 29 | this.invalidateLocalCache(key); 30 | } 31 | }); 32 | this.logger.debug("✅ Cache ready") 33 | } 34 | 35 | private invalidateLocalCache(key: string) { 36 | const entry = this.localCache.get(key); 37 | if (entry) { 38 | this.logger.debug(`Invalidating ${key}`); 39 | clearTimeout(entry.timeoutId); 40 | this.localCache.delete(key); 41 | } 42 | } 43 | 44 | private scheduleNextInvalidation() { 45 | if (this.invalidationTimeout) { 46 | clearTimeout(this.invalidationTimeout); 47 | } 48 | 49 | const nextEntry = this.cacheQueue.peek(); 50 | if (nextEntry) { 51 | const delay = nextEntry.expiry - Date.now(); 52 | this.invalidationTimeout = setTimeout(() => { 53 | while (this.cacheQueue.size() > 0 && (this.cacheQueue.peek() as { key: string, expiry: number }).expiry <= Date.now()) { 54 | const entry = this.cacheQueue.dequeue(); 55 | this.Client.del((entry as { key: string, expiry: number }).key).then(() => null); 56 | this.logger.debug(`Invalidated cache key after TTL: ${(entry as { key: string, expiry: number }).key}`); 57 | } 58 | this.scheduleNextInvalidation(); 59 | }, delay); 60 | } 61 | } 62 | 63 | private setLocalCache(key: string, value: string) { 64 | const timeoutId = setTimeout(() => { 65 | this.localCache.delete(key); 66 | }, this.TTL); 67 | this.localCache.set(key, { value, expiresAt: Date.now() + this.TTL, timeoutId }); 68 | const expiry = Date.now() + this.TTL; 69 | this.cacheQueue.enqueue({ key, expiry }); 70 | this.scheduleNextInvalidation(); 71 | } 72 | 73 | async get(key: string): Promise { 74 | const localEntry = this.localCache.get(key); 75 | if (localEntry && localEntry.expiresAt > Date.now()) { 76 | this.logger.debug(`Found entry ${key} in local cache, and returned it`); 77 | return localEntry.value; 78 | } 79 | 80 | const value = await this.Client.get(key); 81 | if (value !== null) { 82 | this.logger.debug(`Found entry ${key} in global cache, wrote to local, and returned it`); 83 | this.setLocalCache(key, value); 84 | return value; 85 | } 86 | 87 | this.logger.debug(`${key} wasn't found in cache, returning null`); 88 | return null; 89 | } 90 | 91 | async set(key: string, value: string): Promise { 92 | this.logger.debug(`Setting entry ${key} in cache`); 93 | await this.Client.set(key, value, { EX: this.TTL / 1000 }); 94 | await this.Client.publish('cache-updates', JSON.stringify({ action: 'set', key })); 95 | this.setLocalCache(key, value); 96 | } 97 | 98 | async delete(key: string): Promise { 99 | this.logger.debug(`Deleting entry ${key} in cache`); 100 | await this.Client.del(key); 101 | await this.Client.publish('cache-updates', JSON.stringify({ action: 'delete', key })); // Use Client for publishing 102 | this.invalidateLocalCache(key); 103 | } 104 | 105 | async DBget(key: string): Promise { 106 | key = `DB_${key}`; 107 | return this.get(key); 108 | } 109 | 110 | async DBset(key: string, value: string): Promise { 111 | key = `DB_${key}`; 112 | await this.set(key, value); 113 | } 114 | 115 | async DBdelete(key: string): Promise { 116 | key = `DB_${key}`; 117 | await this.delete(key); 118 | } 119 | 120 | async DBkeys(): Promise { 121 | const keyFilter = `DB_*`; 122 | const keys: string[] = []; 123 | this.logger.debug(`Returning database keys in cache`); 124 | const keysIterator = this.Client.scanIterator({ 125 | MATCH: keyFilter 126 | }); 127 | for await (const key of keysIterator) keys.push(key); 128 | return keys; 129 | } 130 | } -------------------------------------------------------------------------------- /structures/Database.ts: -------------------------------------------------------------------------------- 1 | import {Embed, Prisma as PrismaTypes, PrismaClient, Settings} from '@prisma/client'; 2 | 3 | import { Config, config } from '../config/config'; 4 | import { ErryCacheManager } from './Cache'; 5 | import { Logger } from './Logger'; 6 | 7 | export class ErryDatabase extends PrismaClient { 8 | public config: Config 9 | public cache: prismaCacheMiddleware 10 | public logger: Logger 11 | constructor(botCache: ErryCacheManager, options?: PrismaTypes.PrismaClientOptions) { 12 | super(options) 13 | this.config = config 14 | this.cache = new prismaCacheMiddleware({ 15 | useAllModels: true, 16 | defaultCacheActions: [ "findUnique", "findFirst", "findMany", "count", "aggregate", "groupBy", "findRaw", "aggregateRaw" ], 17 | cache: botCache 18 | }) 19 | this.logger = new Logger({ prefix: " Erry DB ", ...this.config.logLevel }); 20 | this.$use(this.cache.handle) 21 | } 22 | public async init(): Promise { 23 | await this.$connect() 24 | this.logger.debug("✅ Database ready") 25 | } 26 | public async getPing(): Promise { 27 | const start = performance.now(); 28 | //await this.$runCommandRaw({ ping: 1 }) // ONLY FOR MONGODB 29 | await this.$queryRaw`SELECT 1`; // ANY SQL 30 | return (performance.now() - start).toFixed(2); 31 | } 32 | public async createGuildDatabase(guild_id: string): Promise { 33 | this.logger.debug(`Creating database for guild ${guild_id}`) 34 | const key = {where: {guildId: guild_id}} 35 | 36 | const settingsDb = await this.settings.findUnique(key) 37 | if (!settingsDb || !settingsDb.language) { 38 | this.logger.debug(`Creating settings table for guild ${guild_id}`) 39 | await this.settings.create({ 40 | data: { 41 | ...this.InitialSettingsDatabase, 42 | guildId: guild_id 43 | } 44 | }) 45 | } 46 | 47 | const es = await this.embed.findUnique(key) 48 | if (!es || !es.color) { 49 | this.logger.debug(`Creating embed table for guild ${guild_id}`) 50 | await this.embed.create({ 51 | data: { 52 | ...this.InitialEmbedDatabase, 53 | guildId: guild_id 54 | } 55 | }) 56 | } 57 | this.logger.debug(`Creating database finished for guild ${guild_id}`) 58 | return true; 59 | } 60 | public async removeGuildDatabase(guild_id: string): Promise { 61 | this.logger.debug(`Removing database for guild ${guild_id}`) 62 | const key = {where: {guildId: guild_id}} 63 | 64 | this.logger.debug(`Deleting embed table for guild ${guild_id}`) 65 | await this.embed.deleteMany(key) 66 | 67 | this.logger.debug(`Deleting settings table for guild ${guild_id}`) 68 | await this.settings.deleteMany(key) 69 | 70 | this.logger.debug(`Removing database finished for guild ${guild_id}`) 71 | return true; 72 | } 73 | public get InitialSettingsDatabase(): Settings { 74 | return { 75 | guildId: "0", // placeholder 76 | language: this.config.defaultLanguage 77 | } 78 | } 79 | public get InitialEmbedDatabase(): Embed { 80 | return { 81 | guildId: "0", // placeholder 82 | color: String(this.config.embed.color), 83 | wrongcolor: String(this.config.embed.wrongcolor), 84 | warncolor: String(this.config.embed.warncolor), 85 | footertext: String(this.config.embed.footertext), 86 | footericon: String(this.config.embed.footericon), 87 | } 88 | } 89 | } 90 | 91 | export const defaultMutationMethods = [ 92 | "create", 93 | "createMany", 94 | "update", 95 | "updateMany", 96 | "upsert", 97 | "delete", 98 | "deleteMany", 99 | "executeRawUnsafe", 100 | ]; 101 | 102 | class prismaCacheMiddleware { 103 | private defaultCacheActions: string[]; 104 | private readonly useAllModels: boolean; 105 | private toCache: { 106 | model: string, 107 | actions: string[], 108 | prefix?: string 109 | }[]; 110 | private cache: ErryCacheManager; 111 | logger: Logger 112 | config: Config 113 | constructor(options: CacheOptions){ 114 | this.toCache = options?.toCache ?? []; 115 | this.defaultCacheActions = options.defaultCacheActions ?? []; 116 | this.useAllModels = options.useAllModels ?? !options?.toCache?.length; 117 | this.cache = options.cache 118 | this.config = config 119 | this.logger = new Logger({ prefix: "Erry DB Cache", ...this.config.logLevel }); 120 | } 121 | 122 | public handle = async (params: MiddlewareParameters, next: (params: MiddlewareParameters) => Promise) => { 123 | let result: any = null; 124 | const instance = (this.useAllModels && this.defaultCacheActions.includes(params.action)) || this.toCache?.find?.(instance => instance.model === params.model && (this.defaultCacheActions.includes(params.action) || instance.actions.includes(params.action))) 125 | if(instance){ 126 | const data = typeof instance === "object" ? instance : { model: params.model, prefix: "" }; 127 | 128 | const cacheKey = `${data.prefix ? `${data.prefix}-`: ``}${params.model}:${params.action}:${JSON.stringify(params.args)}`; 129 | const findCache = await this.cache.DBget(cacheKey); 130 | 131 | if(findCache) { 132 | try { 133 | result = JSON.parse(findCache); 134 | this.logger.debug(`${params.model}.${params.action}() received data from Cache`); 135 | } catch(e) { 136 | console.error(e); 137 | } 138 | } else { 139 | result = await next(params); 140 | this.logger.debug("Found in db and now storing it in:", cacheKey) 141 | await this.cache.DBset(cacheKey, JSON.stringify(result, (_, v) => (typeof v === "bigint" ? v.toString() : v))) 142 | } 143 | } else this.logger.debug(`Could not find instance for ${params.model}`) 144 | 145 | if(!result) { 146 | result = await next(params); 147 | } 148 | if (defaultMutationMethods.includes(params.action)) { 149 | const keysData = Array.from(await this.cache.DBkeys()).filter(k => k.includes(`${params.model}:`)); 150 | let keys: string[] = []; 151 | if(params.args.where) { 152 | const filtered = keysData.filter((k:string) => k.includes(JSON.stringify(params.args.where)) || k.includes("findMany")) 153 | keys = filtered.length ? filtered : keysData; 154 | } 155 | for(const key of keys) await this.cache.DBdelete(key); 156 | this.logger.debug(`Invalidated ${keys.length} Keys after a mutationAction`) 157 | } 158 | return result; 159 | } 160 | } 161 | 162 | export interface CacheOptions { 163 | useAllModels: boolean; 164 | cache: ErryCacheManager 165 | defaultCacheActions?: string[]; 166 | debug?: boolean; 167 | toCache?: { 168 | model: string, 169 | actions: string[], 170 | prefix?: string 171 | }[], 172 | } 173 | export type MiddlewareParameters = { 174 | model?: PrismaTypes.ModelName; 175 | action: PrismaTypes.PrismaAction; 176 | args: any; 177 | dataPath: string[]; 178 | runInTransaction: boolean; 179 | } -------------------------------------------------------------------------------- /structures/Functions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActivityType, ChannelType, EmbedAuthorOptions, EmbedBuilder, EmbedFooterOptions, Guild, 3 | GuildChannel, Locale, parseEmoji, PartialEmoji, PermissionsBitField 4 | } from 'discord.js'; 5 | 6 | import { Config, config, Embed } from '../config/config'; 7 | import { BotCounters, emojiMatches } from '../utils/otherTypes'; 8 | import { BotClient } from './BotClient'; 9 | 10 | export function isValidSnowflake(s: string): boolean { 11 | return /^(\d{17,19})$/ig.test((s || "").trim()); 12 | } 13 | 14 | export class ErryFunctions { 15 | public client: BotClient 16 | private status: number 17 | public config: Config 18 | public isValidSnowflake: (s: string) => boolean; 19 | constructor(client: BotClient) { 20 | this.client = client; 21 | this.status = 0 22 | this.config = config 23 | this.isValidSnowflake = isValidSnowflake 24 | } 25 | public formatMS(millis: number, ls: Locale): string { 26 | let localization = { 27 | m: this.client.lang.translate("common.metrics.minutesShort", ls), 28 | s: this.client.lang.translate("common.metrics.secondsShort", ls), 29 | h: this.client.lang.translate("common.metrics.hoursShort", ls), 30 | seconds: this.client.lang.translate("common.metrics.seconds", ls) 31 | } 32 | let val = millis < 0; 33 | if (millis < 0) { 34 | millis = -millis; 35 | } 36 | let s: number | string = Math.floor((millis / 1000) % 60); 37 | let m: number | string = Math.floor((millis / (1000 * 60)) % 60); 38 | let h: number | string = Math.floor((millis / (1000 * 60 * 60)) % 24); 39 | h = h < 10 ? "0" + h : h.toString(); 40 | m = m < 10 ? "0" + m : m.toString(); 41 | s = s < 10 ? "0" + s : s.toString(); 42 | if (h == "00" || h == "0") return val ? "-" + m + `${localization.m} ` + s + `${localization.s} | ` + "-" + Math.floor((millis / 1000)) + ` ${localization.seconds}` : m + `${localization.m} ` + s + `${localization.s} | ` + Math.floor((millis / 1000)) + " Seconds"; 43 | else return val ? "-" + h + `${localization.h} ` + m + `${localization.m} ` + s + `${localization.s} | ` + "-" + Math.floor((millis / 1000)) + ` ${localization.seconds}` : h + `${localization.h} ` + m + `${localization.m} ` + s + `${localization.s} | ` + Math.floor((millis / 1000)) + " Seconds"; 44 | } 45 | public parseEmojis(stringInput: string, filterDupes: boolean): { str: string; parsed: PartialEmoji | null; }[] { 46 | const matches = [...stringInput.matchAll(emojiMatches)]; 47 | if(!matches.length) return []; 48 | const matchedEmojis = matches.map(x => { 49 | const [unicode, animated, name, id] = x.slice(1); 50 | const str = id && name ? `<${animated||""}:${name}:${id}>` : unicode; 51 | return { str, parsed: parseEmoji(str) } 52 | }); 53 | return filterDupes ? matchedEmojis.reduce((a: { str: string; parsed: PartialEmoji | null; }[], c: { str: string; parsed: PartialEmoji | null; }) => !a.find(item => item.str === c.str) ? a.concat([c]) : a, []) : matchedEmojis; 54 | } 55 | public async statusUpdater(): Promise { 56 | const shardIds = this.client.cluster.shardList; 57 | const { guilds, members } = await this.client.cluster.broadcastEval("this.counters", {timeout: 15000}).then((x: BotCounters[]) => { 58 | return { 59 | guilds: x.map(v => v.guilds || 0).reduce((a, b) => a + b, 0), 60 | members: x.map(v => v.members || 0).reduce((a, b) => a + b, 0) 61 | } 62 | }).catch((e) => { 63 | this.client.logger.error(e); 64 | return { guilds: 0, members: 0 } 65 | }) 66 | this.status + 1 >= this.config.status.activities.length ? this.status = 0 : this.status += 1 67 | for (let i = shardIds.length - 1; i >= 0; i--) { 68 | const shardId = shardIds[i]; 69 | this.client.logger.debug(`Updating status to index ${this.status} on shard #${shardId}`) 70 | this.client.user?.setPresence({ 71 | activities: [{ 72 | name: `${this.config.status.activities[this.status].text}` 73 | //.replaceAll("{commandsused}", stats?.commandsUsed) 74 | //.replaceAll("{songsplayed}", stats?.songsPlayed) 75 | .replaceAll("{guilds}", String(guilds)) 76 | .replaceAll("{members}", String(members)) 77 | .replaceAll("{shard}", String(shardId)) 78 | .replaceAll("{cluster}", String(this.client.cluster.id)), 79 | type: ActivityType[this.config.status.activities[this.status].type], 80 | url: this.config.status.activities[this.status].url || undefined 81 | }], 82 | status: this.config.status.status 83 | }); 84 | } 85 | } 86 | public uniqueArray(arr: any[]): any[] { 87 | let date = Date.now() 88 | const result = arr.filter((element, index) => arr.indexOf(element) === index); 89 | this.client.logger.debug(`Checked for duplicates in ${Date.now()-date}ms`) 90 | return result; 91 | } 92 | public checkPermsForGuild(guild: Guild): {status: boolean, missing?: string[]} { 93 | let me = guild.members.me 94 | if (!me) return {status: true}; 95 | let missing = [] 96 | if (me.permissions.has(PermissionsBitField.Flags.SendMessages)) missing.push(PermissionsBitField.Flags.SendMessages); 97 | if (me.permissions.has(PermissionsBitField.Flags.EmbedLinks)) missing.push(PermissionsBitField.Flags.EmbedLinks); 98 | if (me.permissions.has(PermissionsBitField.Flags.AttachFiles)) missing.push(PermissionsBitField.Flags.AttachFiles); 99 | if (me.permissions.has(PermissionsBitField.Flags.Connect)) missing.push(PermissionsBitField.Flags.Connect); 100 | if (me.permissions.has(PermissionsBitField.Flags.ManageChannels)) missing.push(PermissionsBitField.Flags.ManageChannels); 101 | if (me.permissions.has(PermissionsBitField.Flags.ManageMessages)) missing.push(PermissionsBitField.Flags.ManageMessages); 102 | if (me.permissions.has(PermissionsBitField.Flags.Speak)) missing.push(PermissionsBitField.Flags.Speak); 103 | if (me.permissions.has(PermissionsBitField.Flags.UseExternalEmojis)) missing.push(PermissionsBitField.Flags.UseExternalEmojis); 104 | if (missing?.length >= 1) return {status: false, missing: new PermissionsBitField(missing).toArray()}; 105 | return {status: true}; 106 | } 107 | public checkPermsForChannel(channel: GuildChannel): {status: boolean, missing?: string[]} { 108 | const user = channel.guild.members.me 109 | if (user) var me = channel.permissionsFor(user) 110 | else return {status: true}; 111 | let missing = [] 112 | if (textBasedCats.includes(channel.type)) { 113 | if (me.has(PermissionsBitField.Flags.SendMessages)) missing.push(PermissionsBitField.Flags.SendMessages); 114 | if (me.has(PermissionsBitField.Flags.EmbedLinks)) missing.push(PermissionsBitField.Flags.EmbedLinks); 115 | if (me.has(PermissionsBitField.Flags.AttachFiles)) missing.push(PermissionsBitField.Flags.AttachFiles); 116 | if (me.has(PermissionsBitField.Flags.ManageChannels)) missing.push(PermissionsBitField.Flags.ManageChannels); 117 | if (me.has(PermissionsBitField.Flags.ManageMessages)) missing.push(PermissionsBitField.Flags.ManageMessages); 118 | if (me.has(PermissionsBitField.Flags.UseExternalEmojis)) missing.push(PermissionsBitField.Flags.UseExternalEmojis); 119 | } 120 | if (voiceBasedCats.includes(channel.type)) { 121 | if (me.has(PermissionsBitField.Flags.SendMessages)) missing.push(PermissionsBitField.Flags.SendMessages); 122 | if (me.has(PermissionsBitField.Flags.EmbedLinks)) missing.push(PermissionsBitField.Flags.EmbedLinks); 123 | if (me.has(PermissionsBitField.Flags.AttachFiles)) missing.push(PermissionsBitField.Flags.AttachFiles); 124 | if (me.has(PermissionsBitField.Flags.Connect)) missing.push(PermissionsBitField.Flags.Connect); 125 | if (me.has(PermissionsBitField.Flags.ManageChannels)) missing.push(PermissionsBitField.Flags.ManageChannels); 126 | if (me.has(PermissionsBitField.Flags.ManageMessages)) missing.push(PermissionsBitField.Flags.ManageMessages); 127 | if (me.has(PermissionsBitField.Flags.Speak)) missing.push(PermissionsBitField.Flags.Speak); 128 | if (me.has(PermissionsBitField.Flags.UseExternalEmojis)) missing.push(PermissionsBitField.Flags.UseExternalEmojis); 129 | } 130 | if (missing?.length >= 1) return {status: false, missing: new PermissionsBitField(missing).toArray()}; 131 | return {status: true}; 132 | } 133 | public getFooter(es: Embed, stringurl?: string|null, customText?: string|null): EmbedFooterOptions { 134 | let text = es.footertext; 135 | let iconURL: string|undefined = stringurl ? stringurl : es.footericon; 136 | if (customText) text = customText 137 | if(!text || text.length < 1) text = `${this.client.user?.username || this.config.botName} | By: rocky_pup`; 138 | if(!iconURL || iconURL.length < 1) iconURL = `${this.client.user?.displayAvatarURL()}`; 139 | iconURL = iconURL.trim(); 140 | text = text.trim().substring(0, 2000); 141 | if(!iconURL.startsWith("https://") && !iconURL.startsWith("http://")) iconURL = this.client.user?.displayAvatarURL(); 142 | if(![".png", ".jpg", ".wpeg", ".webm", ".gif", ".webp"].some(d => iconURL?.toLowerCase().endsWith(d))) iconURL = this.client.user?.displayAvatarURL(); 143 | return { text, iconURL } 144 | }; 145 | public getAuthor(authorname: string, authoricon?: string|null, authorurl?: string|null): EmbedAuthorOptions { 146 | let name = authorname; 147 | let iconURL = authoricon; 148 | let url = authorurl; 149 | if(!iconURL || iconURL.length < 1) iconURL = `${this.client.user?.displayAvatarURL()}`; 150 | if(!url || url.length < 1) url = `https://dsc.gg/banditcamp`; 151 | iconURL = iconURL.trim(); 152 | name = `${name.trim().substring(0, 25)}` 153 | if(!url.startsWith("https://") && !url.startsWith("http://")) url = `https://dsc.gg/banditcamp`; 154 | if(!iconURL.startsWith("https://") && !iconURL.startsWith("http://")) iconURL = this.client.user?.displayAvatarURL(); 155 | if(![".png", ".jpg", ".wpeg", ".webm", ".gif"].some(d => iconURL?.toLowerCase().endsWith(d))) iconURL = this.client.user?.displayAvatarURL(); 156 | return { name: name, iconURL: iconURL, url: url } 157 | }; 158 | } 159 | 160 | export const textBasedCats = [ChannelType.GuildText, ChannelType.AnnouncementThread, ChannelType.PublicThread, ChannelType.PrivateThread, ChannelType.GuildCategory, ChannelType.GuildAnnouncement]; 161 | export const voiceBasedCats = [ChannelType.GuildStageVoice, ChannelType.GuildVoice]; 162 | 163 | export class ErryErrorEmbed extends EmbedBuilder { 164 | constructor(es: Embed, exclude?: {footer?: boolean, timestamp?: boolean}) { 165 | super(); 166 | this.setColor(es.wrongcolor); 167 | !exclude?.footer && this.setFooter({text: es.footertext, iconURL: !es.footericon || es.footericon == "" ? undefined : es.footericon}) 168 | !exclude?.timestamp && this.setTimestamp() 169 | } 170 | } 171 | export class ErrySuccessEmbed extends EmbedBuilder { 172 | constructor(es: Embed, exclude?: {footer?: boolean, timestamp?: boolean}) { 173 | super(); 174 | this.setColor(es.color); 175 | !exclude?.footer && this.setFooter({text: es.footertext, iconURL: !es.footericon || es.footericon == "" ? undefined : es.footericon}) 176 | !exclude?.timestamp && this.setTimestamp() 177 | } 178 | } 179 | export class ErryWarningEmbed extends EmbedBuilder { 180 | constructor(es: Embed, exclude?: {footer?: boolean, timestamp?: boolean}) { 181 | super(); 182 | this.setColor(es.warncolor); 183 | !exclude?.footer && this.setFooter({text: es.footertext, iconURL: !es.footericon || es.footericon == "" ? undefined : es.footericon}) 184 | !exclude?.timestamp && this.setTimestamp() 185 | } 186 | } -------------------------------------------------------------------------------- /structures/Language.ts: -------------------------------------------------------------------------------- 1 | import { Locale } from 'discord.js'; 2 | import { promises } from 'fs'; 3 | import { readdir } from 'fs/promises'; 4 | 5 | import { Config, config } from '../config/config.js'; 6 | import { Emojis, emojis } from '../config/emoji.js'; 7 | import { commandLocalizations } from '../utils/otherTypes.js'; 8 | import { Logger } from './Logger.js'; 9 | 10 | export class ErryLanguage { 11 | public config: Config 12 | public emoji: Emojis 13 | public logger: Logger 14 | 15 | constructor() { 16 | this.config = config 17 | this.emoji = emojis 18 | this.logger = new Logger({prefix: "Erry Language", ...this.config.logLevel}) 19 | } 20 | 21 | public translate(key: string, language: Locale, additional?: { 22 | [key: string]: string 23 | }, replace?: boolean): string { 24 | try { 25 | let str 26 | try { 27 | str = this.get[language] as NestedLanguageType 28 | for (let keyy of key.split(".")) { 29 | str = str[keyy] as NestedLanguageType 30 | } 31 | } catch (e) { 32 | str = this.get[this.config.defaultLanguage] as NestedLanguageType 33 | for (let keyy of key.split(".")) { 34 | str = str[keyy] as NestedLanguageType 35 | } 36 | } 37 | if (typeof str == "object") { 38 | let keys = Object.keys(str) 39 | for (const key of keys) { 40 | if (typeof str[key] != "string") continue; 41 | if (additional) { 42 | for (let placeholder in additional) { 43 | str[key] = (str[key] as string).replaceAll(`{{${placeholder}}}`, !replace ? additional[placeholder] : "") 44 | } 45 | } 46 | for (let placeholder in this.emoji) { 47 | str[key] = (str[key] as string).replaceAll(`{{Emoji_${placeholder}}}`, !replace ? this.emoji[placeholder] : "") 48 | } 49 | str[key] = (str[key] as string).replaceAll(/\s*\{{.*?\\}}\s*/g, "") 50 | } 51 | } else { 52 | if (additional) { 53 | for (let placeholder in additional) { 54 | str = (str as string).replaceAll(`{{${placeholder}}}`, !replace ? additional[placeholder] : "") 55 | } 56 | } 57 | for (let placeholder in this.emoji) { 58 | str = (str as string).replaceAll(`{{Emoji_${placeholder}}}`, !replace ? this.emoji[placeholder] : "") 59 | } 60 | str = (str as string).replaceAll(/\s*\{{.*?\\}}\s*/g, "") 61 | } 62 | return (str ? str : "") as string 63 | } catch (e) { 64 | console.error(e) 65 | this.logger.stringError(`Error in key "${key}", language: "${language}"`) 66 | return "" 67 | } 68 | }; 69 | 70 | public get get() { 71 | return languages 72 | } 73 | 74 | public async init(path = "/languages/") { 75 | const dirs = await readdir(`${process.cwd()}${path}`) 76 | for (const dir of dirs) { 77 | const curPath = `${process.cwd()}${path}/${dir}`; 78 | const name = dir.split(".json")[0]; 79 | if (!(Object.values(Locale) as string[]).includes(name)) { 80 | this.logger.warn("❌ Unsupported language detected: ", name) 81 | continue; 82 | } 83 | const language = JSON.parse((await promises.readFile(curPath)).toString()); 84 | 85 | languages[dir.split(".json")[0]] = language 86 | this.logger.debug(`✅ Language Loaded: ${dir.split(".json")[0]}`) 87 | } 88 | } 89 | 90 | public translatePermissions(permissionsArray: string[], ls: Locale) { 91 | if (!permissionsArray || permissionsArray.length <= 0) return this.translate("common.error", ls); 92 | let result = permissionsArray.map((permission) => { 93 | return this.translate(`common.permissions.${permission}`, ls) 94 | }) 95 | return result; 96 | } 97 | 98 | public getSlashCommandName(path: string): string { 99 | this.logger.debug(`getSlashCommandName was just called with path: ${path}`); 100 | const keys = path.split('.'); 101 | let current: NestedLanguageType | string = (languages[config.defaultLanguage] as NestedLanguageType)?.commands; 102 | 103 | if (typeof current === 'undefined') { 104 | throw `You provided wrong default language in config ${config.defaultLanguage}` 105 | } 106 | 107 | for (const key of keys) { 108 | if (typeof current === 'string' || !(key in current)) { 109 | throw `You provided wrong command localizations path (${path}), or this path is not found in default language in config (${config.defaultLanguage})` 110 | } 111 | current = current[key]; 112 | } 113 | 114 | if (!(current as NestedLanguageType).slashLocalizations || !((current as NestedLanguageType).slashLocalizations as LanguageCommandLocalizations)?.name) 115 | throw `No name found in path (${path}), or this path is not found in default language in config ${config.defaultLanguage}` 116 | 117 | return ((current as NestedLanguageType).slashLocalizations as LanguageCommandLocalizations)?.name ?? "undefined"; 118 | } 119 | 120 | public getSlashCommandDescription(path: string): string { 121 | this.logger.debug(`getSlashCommandDescription was just called with path: ${path}`); 122 | const keys = path.split('.'); 123 | let current: NestedLanguageType | string = (languages[config.defaultLanguage] as NestedLanguageType)?.commands; 124 | 125 | if (typeof current === 'undefined') { 126 | throw `You provided wrong default language in config ${config.defaultLanguage}` 127 | } 128 | 129 | for (const key of keys) { 130 | if (typeof current === 'string' || !(key in current)) { 131 | throw `You provided wrong command localizations path (${path}), or this path is not found in default language in config (${config.defaultLanguage})` 132 | } 133 | current = current[key]; 134 | } 135 | 136 | if (!(current as NestedLanguageType).slashLocalizations || !((current as NestedLanguageType).slashLocalizations as LanguageCommandLocalizations)?.description) 137 | throw `No description found in path (${path}), or this path is not found in default language in config ${config.defaultLanguage}` 138 | 139 | return ((current as NestedLanguageType).slashLocalizations as LanguageCommandLocalizations)?.description ?? "Description not provided"; 140 | } 141 | 142 | public getSlashCommandLocalizations(path: string): commandLocalizations[] { 143 | this.logger.debug(`getSlashCommandLocalizations was just called with path: ${path}`); 144 | const keys = path.split('.'); 145 | let results: commandLocalizations[] = []; 146 | 147 | // Get the default language data once to use in fallback cases 148 | const defaultLanguage = config.defaultLanguage; 149 | const defaultLocalization = (languages[defaultLanguage] as NestedLanguageType)?.commands; 150 | 151 | for (const language of Object.keys(languages)) { 152 | let current: NestedLanguageType | string = (languages[language] as NestedLanguageType)?.commands; 153 | 154 | // Navigate through the keys in the path, even if current is undefined 155 | for (const key of keys) { 156 | if (typeof current === 'string' || !(key in current)) { 157 | current = "undefined"; 158 | break; 159 | } 160 | current = current[key]; 161 | } 162 | 163 | let commandName = `cmd-${keys[keys.length - 1]}-${language}`; // Shortened to last part of the path + language 164 | let commandDescription = "No Description Provided"; // Default description if no specific description is found 165 | 166 | if (current) { 167 | // Get the specific command data if exists 168 | const commandData = (current as NestedLanguageType).slashLocalizations as LanguageCommandLocalizations; 169 | commandName = commandData?.name ?? `cmd-${keys[keys.length - 1]}-${language}`; 170 | commandDescription = commandData?.description ?? "No Description Provided"; 171 | } else if (language !== defaultLanguage) { 172 | // If not found, fallback to default language 173 | let fallback = defaultLocalization; 174 | 175 | // Navigate through the keys for the fallback language 176 | for (const key of keys) { 177 | if (typeof fallback === 'string' || !(key in fallback)) { 178 | fallback = "undefined"; 179 | break; 180 | } 181 | fallback = fallback[key]; 182 | } 183 | 184 | if (fallback) { 185 | const fallbackCommand = (fallback as NestedLanguageType).slashLocalizations as LanguageCommandLocalizations; 186 | commandName = fallbackCommand?.name ?? `cmd-${keys[keys.length - 1]}-${language}`; 187 | commandDescription = fallbackCommand?.description ?? "No Description Provided"; 188 | } 189 | } 190 | 191 | // Ensure the command name length is within limits 192 | commandName = commandName.slice(0, 100); // Trim to 100 characters 193 | 194 | // Push the result for this language 195 | results.push({ 196 | language: language as Locale, 197 | name: commandName, 198 | description: commandDescription, 199 | }); 200 | } 201 | 202 | return results; 203 | } 204 | 205 | public getSlashCommandOptionName(path: string, number: number): string { 206 | this.logger.debug(`getSlashCommandOptionName was just called with path: ${path} and number: ${number}`); 207 | const keys = path.split('.'); 208 | let current: NestedLanguageType | string = (languages[config.defaultLanguage] as NestedLanguageType)?.commands; 209 | 210 | if (typeof current === 'undefined') { 211 | throw `You provided wrong default language in config ${config.defaultLanguage}` 212 | } 213 | 214 | for (const key of keys) { 215 | if (typeof current === 'string' || !(key in current)) { 216 | throw `You provided wrong command localizations path (${path}), or this path is not found in default language in config (${config.defaultLanguage})` 217 | } 218 | current = current[key]; 219 | } 220 | 221 | if (!(current as NestedLanguageType).slashLocalizations || !(((current as NestedLanguageType).slashLocalizations as LanguageCommandLocalizations)?.options[`${number}`] as LanguageOptionLocalization).name) 222 | throw `No name found in path (${path}), or this path is not found in default language in config ${config.defaultLanguage}` 223 | 224 | return (((current as NestedLanguageType).slashLocalizations as LanguageCommandLocalizations)?.options[`${number}`] as LanguageOptionLocalization).name ?? "undefined"; 225 | } 226 | 227 | public getSlashCommandOptionDescription(path: string, number: number): string { 228 | this.logger.debug(`getSlashCommandOptionDescription was just called with path: ${path} and number: ${number}`); 229 | const keys = path.split('.'); 230 | let current: NestedLanguageType | string = (languages[config.defaultLanguage] as NestedLanguageType)?.commands; 231 | 232 | if (typeof current === 'undefined') { 233 | throw `You provided wrong default language in config ${config.defaultLanguage}` 234 | } 235 | 236 | for (const key of keys) { 237 | if (typeof current === 'string' || !(key in current)) { 238 | throw `You provided wrong command localizations path (${path}), or this path is not found in default language in config (${config.defaultLanguage})` 239 | } 240 | current = current[key]; 241 | } 242 | 243 | if (!(current as NestedLanguageType).slashLocalizations || !(((current as NestedLanguageType).slashLocalizations as LanguageCommandLocalizations)?.options[`${number}`] as LanguageOptionLocalization).name) 244 | throw `No description found in path (${path}), or this path is not found in default language in config ${config.defaultLanguage}` 245 | 246 | return (((current as NestedLanguageType).slashLocalizations as LanguageCommandLocalizations)?.options[`${number}`] as LanguageOptionLocalization).description ?? "undefined"; 247 | } 248 | 249 | public getSlashCommandOptionLocalizations(path: string, number: number): commandLocalizations[] { 250 | const keys = path.split('.'); 251 | let results: commandLocalizations[] = []; 252 | 253 | // Get the default language data once to use in fallback cases 254 | const defaultLanguage = config.defaultLanguage; 255 | const defaultLocalization = (languages[defaultLanguage] as NestedLanguageType)?.commands; 256 | 257 | // Iterate over the available languages 258 | for (const language of Object.keys(languages)) { 259 | let current: NestedLanguageType | string = (languages[language] as NestedLanguageType)?.commands; 260 | 261 | // Navigate through the keys in the path, even if current is undefined 262 | for (const key of keys) { 263 | if (typeof current === 'string' || !(key in current)) { 264 | current = 'undefined'; 265 | break; 266 | } 267 | current = current[key]; 268 | } 269 | 270 | // Get the specific option for this language 271 | const option = ((current as NestedLanguageType).slashLocalizations as LanguageCommandLocalizations)?.options?.[`${number}`] as undefined | LanguageOptionLocalization; 272 | 273 | if (!option && language !== defaultLanguage) { 274 | let fallback = defaultLocalization; 275 | 276 | // Navigate through the keys for the fallback language 277 | for (const key of keys) { 278 | if (typeof fallback === 'string' || !(key in fallback)) { 279 | fallback = 'undefined'; 280 | break; 281 | } 282 | fallback = fallback[key]; 283 | } 284 | 285 | // Fetch the fallback option 286 | const fallbackOption = ((fallback as NestedLanguageType)?.slashLocalizations as LanguageCommandLocalizations)?.options?.[`${number}`] as undefined | LanguageOptionLocalization; 287 | 288 | // Use fallback with more deterministic naming 289 | results.push({ 290 | language: language as Locale, 291 | name: fallbackOption?.name ? `${fallbackOption.name}-${language}-${number}` : `option-${number}-${language}`, 292 | description: fallbackOption?.description || 'No Description Provided', 293 | }); 294 | } else { 295 | // If the option exists, push it as is 296 | results.push({ 297 | language: language as Locale, 298 | name: option?.name || `option-${number}-${language}`, 299 | description: option?.description || 'No Description Provided', 300 | }); 301 | } 302 | } 303 | 304 | return results; 305 | } 306 | 307 | } 308 | 309 | export const languages: NestedLanguageType = {} 310 | 311 | export type NestedLanguageType = { 312 | [key: string]: string | NestedLanguageType; 313 | }; 314 | 315 | type LanguageCommandLocalizations = { 316 | name: string; 317 | description: string; 318 | options: LanguageOptionsLocalizations 319 | [key: string]: string | NestedLanguageType; 320 | } 321 | type LanguageOptionsLocalizations = { 322 | [key: string]: string | LanguageOptionLocalization; 323 | } 324 | type LanguageOptionLocalization = { 325 | name: string 326 | description: string 327 | [key: string]: string | NestedLanguageType; 328 | } -------------------------------------------------------------------------------- /structures/Logger.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { getInfo } from 'discord-hybrid-sharding'; 3 | import { EmbedBuilder, WebhookClient } from 'discord.js'; 4 | import moment from 'moment'; 5 | 6 | import { config } from '../config/config'; 7 | 8 | const strings = { 9 | Info: " Info ", 10 | Warn: " Warn ", 11 | Log: " Log ", 12 | Error: " Error ", 13 | Success: "Success", 14 | Debug: " Debug " 15 | } 16 | 17 | const default_options = { 18 | prefix: " Erry ", 19 | debug: true, 20 | info: true, 21 | error: true, 22 | success: true, 23 | warn: true, 24 | log: true, 25 | webhook: { 26 | link: config.logLevel.webhook, 27 | debug: true, 28 | info: true, 29 | error: true, 30 | success: true, 31 | warn: true, 32 | log: true, 33 | serverlog: true 34 | } 35 | } 36 | 37 | export interface loggerOptions { 38 | prefix: string, 39 | debug: boolean, 40 | info: boolean, 41 | error: boolean, 42 | success: boolean, 43 | warn: boolean, 44 | log: boolean, 45 | logLevel: number, 46 | webhook: webhookOptions 47 | } 48 | 49 | export type webhookOptions = { 50 | guilds: string, 51 | logs: string, 52 | debug: boolean, 53 | info: boolean, 54 | error: boolean, 55 | success: boolean, 56 | warn: boolean, 57 | log: boolean, 58 | serverlog: boolean 59 | } 60 | 61 | interface inputOption { 62 | prefix: string, 63 | debug?: boolean, 64 | info?: boolean, 65 | error?: boolean, 66 | success?: boolean, 67 | warn?: boolean, 68 | log?: boolean, 69 | logLevel?: number, 70 | webhook?: { 71 | guilds?: string, 72 | logs?: string, 73 | debug?: boolean, 74 | info?: boolean, 75 | error?: boolean, 76 | success?: boolean, 77 | warn?: boolean, 78 | log?: boolean, 79 | serverlog?: boolean 80 | } 81 | } 82 | 83 | export class Logger { 84 | private space: string 85 | public options: loggerOptions 86 | public webhook: WebhookClient | undefined 87 | public guildWebhook: WebhookClient | undefined 88 | constructor(options?: inputOption) { 89 | this.space = chalk.magenta.bold(" [::] ") 90 | this.options = { ...default_options, ...options } as unknown as loggerOptions 91 | if (this.options.webhook.logs) this.webhook = new WebhookClient({ url: this.options.webhook.logs }); 92 | else for (const key of Object.keys(this.options.webhook) as (keyof webhookOptions)[]) {if (key == "logs" || key == "guilds" || key == "serverlog") continue; this.options.webhook[key] = false} 93 | if (this.options.webhook.guilds) this.guildWebhook = new WebhookClient({ url: this.options.webhook.guilds }); 94 | else this.options.webhook.serverlog = false 95 | } 96 | private GetDay() { 97 | return moment().format("DD/MM/YY") 98 | } 99 | private GetTime() { 100 | return moment().format("HH:mm:ss.SS") 101 | } 102 | 103 | public info(...input: string[]): void { 104 | if (!this.options.info) return; 105 | console.log( 106 | [ 107 | chalk.gray(this.GetDay()) + " ", 108 | chalk.gray.bold(this.GetTime()) + " ", 109 | this.options.prefix ? chalk.yellow.bold(this.options.prefix) : "", 110 | this.space, 111 | chalk.cyan.bold(strings.Info), 112 | chalk.cyan.bold(">: "), 113 | chalk.cyan.dim(input.flat().join(" ")) 114 | ].join("") 115 | ) 116 | if (!this.options.webhook.info) return; 117 | this.webhook?.send({ 118 | embeds: [ 119 | new EmbedBuilder() 120 | .setTitle("INFO") 121 | .setDescription(input.flat().join(" ")) 122 | .setColor("Aqua") 123 | ], 124 | username: `Info from cluster #${getInfo().CLUSTER}` 125 | }) 126 | return; 127 | } 128 | 129 | public warn(...input: string[]): void { 130 | if (!this.options.warn) return; 131 | console.log( 132 | [ 133 | chalk.gray(this.GetDay()) + " ", 134 | chalk.gray.bold(this.GetTime()) + " ", 135 | this.options.prefix ? chalk.yellow.bold(this.options.prefix) : "", 136 | this.space, 137 | chalk.yellow.bold(strings.Warn), 138 | chalk.yellow.bold(">: "), 139 | chalk.yellow.dim(input.flat().join(" ")) 140 | ].join("") 141 | ) 142 | if (!this.options.webhook.warn) return; 143 | this.webhook?.send({ 144 | embeds: [ 145 | new EmbedBuilder() 146 | .setTitle("WARNING") 147 | .setDescription(input.flat().join(" ")) 148 | .setColor("Gold") 149 | ], 150 | username: `Warn from cluster #${getInfo().CLUSTER}` 151 | }) 152 | return; 153 | } 154 | 155 | public error(error: Error): void { 156 | if (!this.options.error) return; 157 | console.log( 158 | [ 159 | chalk.gray(this.GetDay()) + " ", 160 | chalk.gray.bold(this.GetTime()) + " ", 161 | this.options.prefix ? chalk.yellow.bold(this.options.prefix) : "", 162 | this.space, 163 | chalk.red.bold(strings.Error), 164 | chalk.red.bold(">: "), 165 | chalk.red.dim( 166 | (error.stack ? [error.stack] : [error.name, error.message]) 167 | .filter(Boolean) 168 | .map(v => v.toString()) 169 | .join("\n") 170 | ) 171 | ].join("") 172 | ) 173 | if (!this.options.webhook.error) return; 174 | this.webhook?.send({ 175 | embeds: [ 176 | new EmbedBuilder() 177 | .setTitle("ERROR") 178 | .setDescription((error.stack ? [error.stack] : [error.name, error.message]) 179 | .filter(Boolean) 180 | .map(v => v.toString()) 181 | .join("\n") 182 | .substring(0, 2000)) 183 | .setColor("Red") 184 | ], 185 | username: `Error on cluster #${getInfo().CLUSTER}` 186 | }) 187 | return; 188 | } 189 | 190 | public stringError(...error: string[]): void { 191 | if (!this.options.error) return; 192 | console.log( 193 | [ 194 | chalk.gray(this.GetDay()) + " ", 195 | chalk.gray.bold(this.GetTime()) + " ", 196 | this.options.prefix ? chalk.yellow.bold(this.options.prefix) : "", 197 | this.space, 198 | chalk.red.bold(strings.Error), 199 | chalk.red.bold(">: "), 200 | chalk.red.dim(error.flat().join(" ")) 201 | ].join("") 202 | ) 203 | if (!this.options.webhook.error) return; 204 | this.webhook?.send({ 205 | embeds: [ 206 | new EmbedBuilder() 207 | .setTitle("ERROR") 208 | .setDescription(error.flat().join(" ").substring(0, 2000)) 209 | .setColor("Red") 210 | ], 211 | username: `Error on cluster #${getInfo().CLUSTER}` 212 | }) 213 | return; 214 | } 215 | 216 | public success(...input: string[]): void { 217 | if (!this.options.success) return; 218 | console.log( 219 | [ 220 | chalk.gray(this.GetDay()) + " ", 221 | chalk.gray.bold(this.GetTime()) + " ", 222 | this.options.prefix ? chalk.yellow.bold(this.options.prefix) : "", 223 | this.space, 224 | chalk.green.bold(strings.Success), 225 | chalk.green.bold(">: "), 226 | chalk.green.dim(input.flat().join(" ")) 227 | ].join("") 228 | ) 229 | if (!this.options.webhook.success) return; 230 | this.webhook?.send({ 231 | embeds: [ 232 | new EmbedBuilder() 233 | .setTitle("SUCCESS") 234 | .setDescription(input.flat().join(" ")) 235 | .setColor("Green") 236 | ], 237 | username: `Success on cluster #${getInfo().CLUSTER}` 238 | }) 239 | return; 240 | } 241 | 242 | public debug(...input: string[]): void { 243 | if (!this.options.debug) return; 244 | console.log( 245 | [ 246 | chalk.gray(this.GetDay()) + " ", 247 | chalk.gray.bold(this.GetTime()) + " ", 248 | this.options.prefix ? chalk.yellow.bold(this.options.prefix) : "", 249 | this.space, 250 | chalk.gray.bold(strings.Debug), 251 | chalk.gray.bold(">: "), 252 | chalk.gray.dim(input.flat().join(" ")) 253 | ].join("") 254 | ) 255 | if (!this.options.webhook.debug) return; 256 | this.webhook?.send({ 257 | embeds: [ 258 | new EmbedBuilder() 259 | .setTitle("DEBUG") 260 | .setDescription(input.flat().join(" ")) 261 | .setColor("Greyple") 262 | ], 263 | username: `Debug from cluster #${getInfo().CLUSTER}` 264 | }) 265 | return; 266 | } 267 | 268 | public log(...input: string[]): void { 269 | if (!this.options.log) return; 270 | console.log( 271 | [ 272 | chalk.gray(this.GetDay()) + " ", 273 | chalk.gray.bold(this.GetTime()) + " ", 274 | this.options.prefix ? chalk.yellow.bold(this.options.prefix) : "", 275 | this.space, 276 | chalk.gray.bold(strings.Log), 277 | chalk.gray.bold(">: "), 278 | chalk.gray.dim(input.flat().join(" ")) 279 | ].join("") 280 | ) 281 | if (!this.options.webhook.log && this.options.webhook.logs) return; 282 | this.webhook?.send({ 283 | embeds: [ 284 | new EmbedBuilder() 285 | .setTitle("LOG") 286 | .setDescription(input.flat().join(" ")) 287 | .setColor("Aqua") 288 | ], 289 | username: `Log from cluster #${getInfo().CLUSTER}` 290 | }) 291 | return; 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /structures/Sharder.ts: -------------------------------------------------------------------------------- 1 | import { Bridge, Client } from 'discord-cross-hosting'; 2 | import { 3 | ClusterManager, evalOptions, HeartbeatManager, ReClusterManager 4 | } from 'discord-hybrid-sharding'; 5 | import { ShardClientUtil } from 'discord.js'; 6 | import { exec as ChildProcessExec } from 'node:child_process'; 7 | 8 | import { config } from '../config/config.ts'; 9 | import { isValidSnowflake } from './Functions.ts'; 10 | import { Logger } from './Logger.ts'; 11 | 12 | const botPath = `${process.cwd()}/bot.ts`; 13 | 14 | const LOGGER = new Logger({ prefix: " ClusterMngr ", ...config.logLevel }); 15 | 16 | export class ErryClusterManager extends ClusterManager { 17 | public checkShardDowntime: NodeJS.Timeout | null; 18 | public bridgeServer: Bridge | null; 19 | public bridgeClient: Client | null; 20 | public logger: Logger; 21 | constructor() { 22 | super(botPath, { 23 | totalShards: 1, 24 | shardsPerClusters: config.bridge_shardsPerCluster === "auto" ? undefined : Number(config.bridge_shardsPerCluster), 25 | shardArgs: [ ], 26 | execArgv: Array.from(process.execArgv), 27 | mode: 'process', 28 | token: config.token, 29 | }); 30 | this.checkShardDowntime = null 31 | this.bridgeServer = null 32 | this.bridgeClient = null 33 | this.logger = LOGGER; 34 | this.extend(new ReClusterManager()); 35 | this.extend(new HeartbeatManager({ interval: 15e3, maxMissedHeartbeats: 5 })); 36 | this.on("debug", (m) => this.logger.debug(`[MANAGER] ${m}`)); 37 | this.init(); 38 | } 39 | 40 | clusterIdOfShardId(shardId: number | string) { 41 | if(typeof shardId === "undefined" || typeof shardId !== "number" || isNaN(shardId)) throw new Error("No valid ShardId Provided") 42 | if(Number(shardId) > this.totalShards) throw new Error("Provided ShardId, is bigger than all Shard Ids"); 43 | const middlePart = Number(shardId) === 0 ? 0 : Number(shardId) / Math.ceil(this.totalShards / this.totalClusters); 44 | return Number(shardId) === 0 ? 0 : (Math.ceil(middlePart) - (middlePart % 1 !== 0 ? 1 : 0)); 45 | } 46 | clusterIdOfGuildId(guildId: string) { 47 | if(!guildId || !isValidSnowflake(guildId)) throw new Error("Provided GuildId, is not a valid GuildId"); 48 | return this.clusterIdOfShardId(this.shardIdOfGuildId(guildId)); 49 | } 50 | shardIdOfGuildId(guildId: string) { 51 | if(!guildId || !isValidSnowflake(guildId)) throw new Error("Provided GuildId, is not a valid GuildId"); 52 | return ShardClientUtil.shardIdForGuildId(guildId, this.totalShards); 53 | } 54 | async evalOnGuild(callBackFunction: Function, guildID: string, options:Partial={}) { 55 | if(!guildID || !isValidSnowflake(guildID)) throw new Error("Provided GuildId, is not a valid GuildId"); 56 | if(typeof options !== "object") throw new Error("Provided Options, must be an object!"); 57 | 58 | options.cluster = this.clusterIdOfGuildId(guildID); 59 | 60 | // @ts-ignore 61 | return this.broadcastEval(callBackFunction, options).then(v => v[0]); 62 | } 63 | overWriteHooks() { 64 | this.hooks.constructClusterArgs = (cluster, args) => { 65 | return [ 66 | ...args, 67 | `| Cluster: #${cluster.id}, Shard${cluster.shardList.length !== 1 ? "s" : ""}: ${cluster.shardList.map(sId => `#${sId}`).join(", ")}` 68 | ]; 69 | } 70 | } 71 | 72 | async initBridgeServer() { 73 | if (!config.bridge_create) return true; 74 | this.bridgeServer = new Bridge({ 75 | listenOptions: { 76 | host: "0.0.0.0" 77 | }, 78 | port: config.bridge_port, 79 | authToken: config.bridge_authToken, 80 | totalShards: config.bridge_totalShards === "auto" ? "auto" : Number(config.bridge_totalShards), 81 | totalMachines: config.bridge_machines, 82 | shardsPerCluster: config.bridge_shardsPerCluster === "auto" ? undefined : Number(config.bridge_shardsPerCluster), 83 | token: config.token 84 | }); 85 | this.bridgeServer.on("debug", (d) => this.logger.debug("[BRIDGE]", d)); 86 | return this.bridgeServer.start(); 87 | } 88 | 89 | async initBridgeClient() { 90 | this.bridgeClient = new Client({ 91 | agent: "bot", 92 | host: config.bridge_create ? "localhost" : config.bridge_host, 93 | port: config.bridge_port, 94 | authToken: config.bridge_authToken, 95 | retries: 360, 96 | rollingRestarts: true 97 | }); 98 | // @ts-ignore 99 | this.bridgeClient.on("debug", (d) => this.logger.debug("[CLIENT]", d)); 100 | this.bridgeClient.on("status", (status) => this.logger.debug(`[CLIENT] Status : ${status}`)); 101 | this.bridgeClient.on("close", (reason) => this.logger.info("[CLIENT] Closed: ", String(reason))); 102 | this.bridgeClient.on("error", (error) => {this.logger.stringError("[CLIENT] Error: "); this.logger.stringError(String(error))}); 103 | // @ts-ignore 104 | this.bridgeClient.listen(this); 105 | return this.bridgeClient.connect(); 106 | } 107 | 108 | async init() { 109 | await this.initBridgeServer(); 110 | await this.initBridgeClient(); 111 | this.overWriteHooks() 112 | this.listenStopManager(); 113 | this.listenSpawningEvents(); 114 | await this.summon(); 115 | } 116 | 117 | listenStopManager() { 118 | // terminate the program if needed 119 | ['SIGINT', 'SIGTERM', 'SIGUSR1', 'SIGUSR2'].forEach(signal => 120 | process.on(signal, () => { 121 | this.logger.info("Terminating main process..."); 122 | process.exit() 123 | }) 124 | ); 125 | // terminate the childs if needed 126 | ["beforeExit", "exit"].forEach(event => 127 | process.on(event, () => { 128 | this.logger.info("Terminating all the shard processes..."); 129 | ChildProcessExec(`pkill -f "${botPath}" -SIGKILL`) 130 | }) 131 | ); 132 | return true; 133 | } 134 | listenSpawningEvents() { 135 | const startTime = process.hrtime(); 136 | return this.on('clusterCreate', cluster => { 137 | this.logger.info(`Launched Cluster #${cluster.id} (${cluster.id+1}/${this.totalClusters} Clusters) [${cluster?.shardList.length}/${this.totalShards} Shards]`) 138 | cluster.on("message", async (msg) => { 139 | // here you can handle custom ipc messages if you want... you can think of a style of that... 140 | }); 141 | // when all clusters are ready... 142 | const clusterAmount = Math.ceil(Number(this.totalShards) / Number(this.shardsPerClusters)); 143 | if(Array.from(this.clusters.values()).filter(c => c.ready).length === clusterAmount) readyManagerEvent(this, startTime); 144 | }); 145 | } 146 | async summon(timeout?:number, delay?:number) { 147 | await this.bridgeClient?.requestShardData() 148 | .then(async e => { 149 | if (!e) return; 150 | if (!e.shardList) return; 151 | this.totalShards = e.totalShards; 152 | this.totalClusters = e.shardList.length; 153 | this.shardList = e.shardList[0]; 154 | this.clusterList = e.clusterList; 155 | await this.spawn({ timeout: timeout ?? -1, delay: delay ?? 7000 }) 156 | }) 157 | } 158 | } 159 | 160 | const readyManagerEvent = async (manager:ErryClusterManager, startTime:[number, number]) => { 161 | LOGGER.info(`All Clusters fully started\nTook ${calcProcessDurationTime(startTime)}ms`); 162 | 163 | if(manager.checkShardDowntime) clearInterval(manager.checkShardDowntime); 164 | 165 | manager.checkShardDowntime = setInterval(async () => { 166 | const res = await manager.broadcastEval(`this.ws.status ? this.ws.reconnect() : 0`).catch(e => { 167 | LOGGER.error(e) 168 | LOGGER.stringError(`Script: "this.ws.status ? this.ws.reconnect() : 0"`) 169 | return [] 170 | }); 171 | 172 | if(res.filter(Boolean).length) LOGGER.debug("[MANAGER_CHECKER] Restarted shards:", String(res)); 173 | return; 174 | }, 180000).unref(); 175 | }; 176 | 177 | export function calcProcessDurationTime(beforeHRTime:[number, number]): number { 178 | const timeAfter = process.hrtime(beforeHRTime); 179 | return Math.floor((timeAfter[0] * 1000000000 + timeAfter[1]) / 10000) / 100; 180 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | "lib": ["ESNext"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "ESNext", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | "moduleResolution": "Node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 83 | 84 | /* Type Checking */ 85 | "strict": true, /* Enable all strict type-checking options. */ 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /utils/functions_init.ts: -------------------------------------------------------------------------------- 1 | import { parse, stringify } from 'envfile'; 2 | import * as fs from 'fs'; 3 | import * as os from 'os'; 4 | 5 | import { config } from '../config/config'; 6 | 7 | let packagejson: any = fs.readFileSync('./package.json') 8 | let envfile: any 9 | let connectionString 10 | let force: boolean 11 | export function INIT(frc: boolean) { 12 | force = frc 13 | try { 14 | envfile = fs.readFileSync('.env') 15 | }catch(e){} 16 | if (envfile) envfile = parse(envfile) 17 | else envfile = {} 18 | try { 19 | connectionString = new URL(config.database || envfile.DATABASE_URL) 20 | }catch(e){ 21 | console.error("Please check if you provided database link in any config or env") 22 | process.exit(1) 23 | } 24 | envfile.DATABASE_URL = envfile.DATABASE_URL === connectionString.toString() || config.database === "" ? envfile.DATABASE_URL : connectionString.toString(); //connectionString.toString() 25 | envfile.BOT_PROCESS_NAME = config.botName 26 | envfile.TOKEN = envfile.TOKEN === config.token || config.token === "" ? envfile.TOKEN : config.token; 27 | envfile.AUTH_KEY = envfile.AUTH_KEY === config.bridge_authToken || config.bridge_authToken === "" ? envfile.AUTH_KEY : config.bridge_authToken; 28 | envfile.REDIS = envfile.REDIS === config.redis || config.redis === "" ? envfile.REDIS : config.redis; 29 | envfile.BRIDGE_HOST = envfile.BRIDGE_HOST === config.bridge_host || config.bridge_host === "" ? envfile.BRIDGE_HOST : config.bridge_host; 30 | envfile.BRIDGE_PORT = envfile.BRIDGE_PORT === config.bridge_port || (!config.bridge_port || config.bridge_port === 0) ? envfile.BRIDGE_PORT : config.bridge_port; 31 | packagejson = JSON.parse(packagejson) 32 | getOperatingSystemCommands() 33 | packagejson = JSON.stringify(packagejson, null, 2); 34 | fs.writeFileSync('./package.json', packagejson); 35 | fs.writeFileSync('.env' ,stringify(envfile) ,{encoding:'utf8', flag:'w'}) 36 | process.exit() 37 | } 38 | 39 | function getOperatingSystemCommands(): void { 40 | const platform: string = os.platform(); 41 | 42 | if (platform.startsWith('win')) { 43 | return loadWindowsCommands(); 44 | } else if (platform.startsWith('linux')) { 45 | return loadLinuxCommands(); 46 | } else { 47 | if (!force) { 48 | console.error("[INIT SETUP] Code can't determine your OS. I'm not sure you can run it! If you are, go to file \"TS_Handler/utils/init.ts\" and set variable \"force\" to true") 49 | process.exit(1) 50 | } 51 | return loadUnknownCommands(); 52 | } 53 | } 54 | function loadLinuxCommands(): void { 55 | packagejson.scripts.start = `FORCE_COLOR=1 pm2 start --name '${config.botName}' npx -- tsx index.ts && pm2 save && pm2 log '${config.botName}'` 56 | packagejson.scripts["start:cmd"] = `FORCE_COLOR=1 npx tsx index.js` 57 | packagejson.scripts.restart = `pm2 restart '${config.botName}' && pm2 log '${config.botName}'` 58 | packagejson.scripts.stop = `pm2 stop '${config.botName}'` 59 | packagejson.scripts.delete = `pm2 delete '${config.botName}' && pm2 save --force` 60 | } 61 | function loadWindowsCommands(): void { 62 | packagejson.scripts.start = `set FORCE_COLOR=1 && pm2 start --name '${config.botName}' npx -- tsx index.ts && pm2 save && pm2 log '${config.botName}'` 63 | packagejson.scripts["start:cmd"] = `set FORCE_COLOR=1 && npx tsx index.js` 64 | packagejson.scripts.restart = `pm2 restart '${config.botName}' && pm2 log '${config.botName}'` 65 | packagejson.scripts.stop = `pm2 stop '${config.botName}'` 66 | packagejson.scripts.delete = `pm2 delete '${config.botName}' && pm2 save --force` 67 | } 68 | function loadUnknownCommands(): void { 69 | packagejson.scripts.start = `pm2 start --name '${config.botName}' npx -- tsx index.ts && pm2 save && pm2 log '${config.botName}'` 70 | packagejson.scripts["start:cmd"] = `npx tsx index.js` 71 | packagejson.scripts.restart = `pm2 restart '${config.botName}' && pm2 log '${config.botName}'` 72 | packagejson.scripts.stop = `pm2 stop '${config.botName}'` 73 | packagejson.scripts.delete = `pm2 delete '${config.botName}' && pm2 save --force` 74 | } -------------------------------------------------------------------------------- /utils/init.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | // CHANGE TO true TO IGNORE OS WARNING 4 | const force = false 5 | 6 | 7 | 8 | 9 | import { INIT } from './functions_init'; 10 | 11 | import("dotenv").then(dotenv => {dotenv.config({path: __dirname+"/.env"}); INIT(force)}); -------------------------------------------------------------------------------- /utils/otherTypes.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AutocompleteInteraction, ChannelType, ChatInputCommandInteraction, CommandInteraction, 3 | ContextMenuCommandInteraction, Locale, LocalizationMap 4 | } from 'discord.js'; 5 | 6 | import { Settings } from '@prisma/client'; 7 | 8 | import { Embed } from '../config/config'; 9 | import { BotClient } from '../structures/BotClient'; 10 | 11 | export interface CommandExport { 12 | name?: string, 13 | description?: string, 14 | defaultPermissions?: bigint, 15 | dmPermissions?: bigint, 16 | mustPermissions?: bigint[], 17 | allowedPermissions?: bigint[], 18 | contexts?: number[], 19 | localizations?: commandLocalizations[], 20 | options?: commandOption[], 21 | category?: string, 22 | cooldown?: CommandCooldown, 23 | mention?: string, 24 | commandId?: string, 25 | slashCommandKey?: string, 26 | execute: (client: BotClient, interaction: ChatInputCommandInteraction, es: Embed, ls: Locale, GuildSettings: Settings) => void; 27 | autocomplete?: (client: BotClient, interaction: AutocompleteInteraction, es: Embed, ls: Locale, GuildSettings: Settings) => void; 28 | } 29 | export interface Command { 30 | name: string, 31 | description: string, 32 | defaultPermissions?: bigint, 33 | dmPermissions?: bigint, 34 | mustPermissions?: bigint[], 35 | allowedPermissions?: bigint[], 36 | localizations?: commandLocalizations[], 37 | options?: commandOption[], 38 | category?: string, 39 | cooldown?: CommandCooldown, 40 | mention?: string, 41 | commandId?: string, 42 | slashCommandKey?: string, 43 | execute: (client: BotClient, interaction: CommandInteraction, es: Embed, ls: Locale, GuildSettings: Settings) => void; 44 | autocomplete?: (client: BotClient, interaction: AutocompleteInteraction, es: Embed, ls: Locale, GuildSettings: Settings) => void; 45 | } 46 | export interface ContextCommand { 47 | name: string, 48 | type: "Message"|"User", 49 | defaultPermissions?: bigint, 50 | dmPermissions?: bigint, 51 | mustPermissions?: bigint[], 52 | allowedPermissions?: bigint[], 53 | localizations?: LocalizationMap, 54 | category?: string, 55 | cooldown?: CommandCooldown, 56 | mention?: string, 57 | commandId?: string, 58 | shortName?: string, 59 | slashCommandKey?: string, 60 | execute: (client: BotClient, interaction: ContextMenuCommandInteraction, es: Embed, ls: Locale, GuildSettings: Settings) => void; 61 | } 62 | export type CommandCooldown = { 63 | user: number 64 | guild: number 65 | } 66 | export type commandLocalizations = { 67 | language: Locale 68 | name: string, 69 | description: string 70 | } 71 | type CommandOptionBase = { 72 | name?: string; 73 | description?: string; 74 | required?: boolean; 75 | autocomplete?: boolean; 76 | localizations?: commandOptionLocalizations[]; 77 | }; 78 | export type CommandOptionString = CommandOptionBase & { 79 | type: "string"; 80 | max?: number, 81 | min?: number, 82 | choices?: never 83 | }; 84 | export type CommandOptionStringChoices = CommandOptionBase & { 85 | type: "stringchoices"; 86 | choices: commandOptionChoiceString[], 87 | max?: number, 88 | min?: number 89 | }; 90 | export type CommandOptionNumber = CommandOptionBase & { 91 | type: "number", 92 | max?: number, 93 | min?: number, 94 | }; 95 | export type CommandOptionNumberChoices = CommandOptionBase & { 96 | type: "numberchoices", 97 | choices: commandOptionChoiceNumber[], 98 | max?: number, 99 | min?: number 100 | }; 101 | export type CommandOptionChannel = CommandOptionBase & { 102 | type: "channel", 103 | channelTypes?: (ChannelType.GuildText | ChannelType.GuildVoice | ChannelType.GuildCategory | ChannelType.GuildAnnouncement | ChannelType.AnnouncementThread | ChannelType.PublicThread | ChannelType.PrivateThread | ChannelType.GuildStageVoice | ChannelType.GuildForum | ChannelType.GuildMedia)[] 104 | }; 105 | export type CommandOptionUser = CommandOptionBase & { 106 | type: "user" 107 | }; 108 | export type CommandOptionRole = CommandOptionBase & { 109 | type: "role" 110 | }; 111 | export type CommandOptionAttachment = CommandOptionBase & { 112 | type: "attachment" 113 | }; 114 | export type commandOption = CommandOptionString | CommandOptionStringChoices | CommandOptionNumber | CommandOptionNumberChoices | CommandOptionChannel | CommandOptionUser | CommandOptionRole | CommandOptionAttachment 115 | 116 | export type commandOptionLocalizations = { 117 | language: Locale 118 | name: string, 119 | description: string 120 | } 121 | export type commandOptionChoiceString = { 122 | name: string; 123 | value: string; 124 | } 125 | 126 | export type commandOptionChoiceNumber = { 127 | name: string; 128 | value: number; 129 | } 130 | 131 | export interface dirSetup { 132 | Folder: string, 133 | name: string, 134 | contexts: number[], 135 | description?: string, 136 | defaultPermissions?: bigint, 137 | dmPermissions?: bigint, 138 | groups?: groupDirSetup[] 139 | localizations?: groupDirSetupLocalizations[] 140 | } 141 | 142 | export type groupDirSetup = { 143 | Folder: string, 144 | name: string, 145 | description?: string, 146 | localizations?: groupDirSetupLocalizations[] 147 | } 148 | 149 | export type groupDirSetupLocalizations = { 150 | language: Locale 151 | name?: string 152 | description: string 153 | } 154 | 155 | export type BotCounters = { 156 | guilds: number, 157 | members: number, 158 | clusterId: number, 159 | shardIds: number[], 160 | ping: number, 161 | uptime: number, 162 | } 163 | 164 | export const emojiMatches = /(?|(?:\ud83d\udc68\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c\udffb|\ud83d\udc68\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc]|\ud83d\udc68\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd]|\ud83d\udc68\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c\udffb|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffc]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffd]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c\udffb|\ud83e\uddd1\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb\udffc]|\ud83e\uddd1\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udffd]|\ud83e\uddd1\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\u200d\ud83e\udd1d\u200d\ud83e\uddd1|\ud83d\udc6b\ud83c[\udffb-\udfff]|\ud83d\udc6c\ud83c[\udffb-\udfff]|\ud83d\udc6d\ud83c[\udffb-\udfff]|\ud83d[\udc6b-\udc6d])|(?:\ud83d[\udc68\udc69])(?:\ud83c[\udffb-\udfff])?\u200d(?:\u2695\ufe0f|\u2696\ufe0f|\u2708\ufe0f|\ud83c[\udf3e\udf73\udf93\udfa4\udfa8\udfeb\udfed]|\ud83d[\udcbb\udcbc\udd27\udd2c\ude80\ude92]|\ud83e[\uddaf-\uddb3\uddbc\uddbd])|(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75]|\u26f9)((?:\ud83c[\udffb-\udfff]|\ufe0f)\u200d[\u2640\u2642]\ufe0f)|(?:\ud83c[\udfc3\udfc4\udfca]|\ud83d[\udc6e\udc71\udc73\udc77\udc81\udc82\udc86\udc87\ude45-\ude47\ude4b\ude4d\ude4e\udea3\udeb4-\udeb6]|\ud83e[\udd26\udd35\udd37-\udd39\udd3d\udd3e\uddb8\uddb9\uddcd-\uddcf\uddd6-\udddd])(?:\ud83c[\udffb-\udfff])?\u200d[\u2640\u2642]\ufe0f|(?:\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83c\udff3\ufe0f\u200d\ud83c\udf08|\ud83c\udff4\u200d\u2620\ufe0f|\ud83d\udc15\u200d\ud83e\uddba|\ud83d\udc41\u200d\ud83d\udde8|\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc6f\u200d\u2640\ufe0f|\ud83d\udc6f\u200d\u2642\ufe0f|\ud83e\udd3c\u200d\u2640\ufe0f|\ud83e\udd3c\u200d\u2642\ufe0f|\ud83e\uddde\u200d\u2640\ufe0f|\ud83e\uddde\u200d\u2642\ufe0f|\ud83e\udddf\u200d\u2640\ufe0f|\ud83e\udddf\u200d\u2642\ufe0f)|[#*0-9]\ufe0f?\u20e3|(?:[©®\u2122\u265f]\ufe0f)|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37\udf21\udf24-\udf2c\udf36\udf7d\udf96\udf97\udf99-\udf9b\udf9e\udf9f\udfcd\udfce\udfd4-\udfdf\udff3\udff5\udff7]|\ud83d[\udc3f\udc41\udcfd\udd49\udd4a\udd6f\udd70\udd73\udd76-\udd79\udd87\udd8a-\udd8d\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa\udecb\udecd-\udecf\udee0-\udee5\udee9\udef0\udef3]|[\u203c\u2049\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23ed-\u23ef\u23f1\u23f2\u23f8-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f8\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714\u2716\u271d\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u2764\u27a1\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299])(?:\ufe0f|(?!\ufe0e))|(?:(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75\udd90]|[\u261d\u26f7\u26f9\u270c\u270d])(?:\ufe0f|(?!\ufe0e))|(?:\ud83c[\udf85\udfc2-\udfc4\udfc7\udfca]|\ud83d[\udc42\udc43\udc46-\udc50\udc66-\udc69\udc6e\udc70-\udc78\udc7c\udc81-\udc83\udc85-\udc87\udcaa\udd7a\udd95\udd96\ude45-\ude47\ude4b-\ude4f\udea3\udeb4-\udeb6\udec0\udecc]|\ud83e[\udd0f\udd18-\udd1c\udd1e\udd1f\udd26\udd30-\udd39\udd3d\udd3e\uddb5\uddb6\uddb8\uddb9\uddbb\uddcd-\uddcf\uddd1-\udddd]|[\u270a\u270b]))(?:\ud83c[\udffb-\udfff])?|(?:\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f|\ud83c\udde6\ud83c[\udde8-\uddec\uddee\uddf1\uddf2\uddf4\uddf6-\uddfa\uddfc\uddfd\uddff]|\ud83c\udde7\ud83c[\udde6\udde7\udde9-\uddef\uddf1-\uddf4\uddf6-\uddf9\uddfb\uddfc\uddfe\uddff]|\ud83c\udde8\ud83c[\udde6\udde8\udde9\uddeb-\uddee\uddf0-\uddf5\uddf7\uddfa-\uddff]|\ud83c\udde9\ud83c[\uddea\uddec\uddef\uddf0\uddf2\uddf4\uddff]|\ud83c\uddea\ud83c[\udde6\udde8\uddea\uddec\udded\uddf7-\uddfa]|\ud83c\uddeb\ud83c[\uddee-\uddf0\uddf2\uddf4\uddf7]|\ud83c\uddec\ud83c[\udde6\udde7\udde9-\uddee\uddf1-\uddf3\uddf5-\uddfa\uddfc\uddfe]|\ud83c\udded\ud83c[\uddf0\uddf2\uddf3\uddf7\uddf9\uddfa]|\ud83c\uddee\ud83c[\udde8-\uddea\uddf1-\uddf4\uddf6-\uddf9]|\ud83c\uddef\ud83c[\uddea\uddf2\uddf4\uddf5]|\ud83c\uddf0\ud83c[\uddea\uddec-\uddee\uddf2\uddf3\uddf5\uddf7\uddfc\uddfe\uddff]|\ud83c\uddf1\ud83c[\udde6-\udde8\uddee\uddf0\uddf7-\uddfb\uddfe]|\ud83c\uddf2\ud83c[\udde6\udde8-\udded\uddf0-\uddff]|\ud83c\uddf3\ud83c[\udde6\udde8\uddea-\uddec\uddee\uddf1\uddf4\uddf5\uddf7\uddfa\uddff]|\ud83c\uddf4\ud83c\uddf2|\ud83c\uddf5\ud83c[\udde6\uddea-\udded\uddf0-\uddf3\uddf7-\uddf9\uddfc\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7\ud83c[\uddea\uddf4\uddf8\uddfa\uddfc]|\ud83c\uddf8\ud83c[\udde6-\uddea\uddec-\uddf4\uddf7-\uddf9\uddfb\uddfd-\uddff]|\ud83c\uddf9\ud83c[\udde6\udde8\udde9\uddeb-\udded\uddef-\uddf4\uddf7\uddf9\uddfb\uddfc\uddff]|\ud83c\uddfa\ud83c[\udde6\uddec\uddf2\uddf3\uddf8\uddfe\uddff]|\ud83c\uddfb\ud83c[\udde6\udde8\uddea\uddec\uddee\uddf3\uddfa]|\ud83c\uddfc\ud83c[\uddeb\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c\uddfe\ud83c[\uddea\uddf9]|\ud83c\uddff\ud83c[\udde6\uddf2\uddfc]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf2d-\udf35\udf37-\udf7c\udf7e-\udf84\udf86-\udf93\udfa0-\udfc1\udfc5\udfc6\udfc8\udfc9\udfcf-\udfd3\udfe0-\udff0\udff4\udff8-\udfff]|\ud83d[\udc00-\udc3e\udc40\udc44\udc45\udc51-\udc65\udc6a-\udc6d\udc6f\udc79-\udc7b\udc7d-\udc80\udc84\udc88-\udca9\udcab-\udcfc\udcff-\udd3d\udd4b-\udd4e\udd50-\udd67\udda4\uddfb-\ude44\ude48-\ude4a\ude80-\udea2\udea4-\udeb3\udeb7-\udebf\udec1-\udec5\uded0-\uded2\uded5\udeeb\udeec\udef4-\udefa\udfe0-\udfeb]|\ud83e[\udd0d\udd0e\udd10-\udd17\udd1d\udd20-\udd25\udd27-\udd2f\udd3a\udd3c\udd3f-\udd45\udd47-\udd71\udd73-\udd76\udd7a-\udda2\udda5-\uddaa\uddae-\uddb4\uddb7\uddba\uddbc-\uddca\uddd0\uddde-\uddff\ude70-\ude73\ude78-\ude7a\ude80-\ude82\ude90-\ude95]|[\u23e9-\u23ec\u23f0\u23f3\u267e\u26ce\u2705\u2728\u274c\u274e\u2753-\u2755\u2795-\u2797\u27b0\u27bf\ue50a])|\ufe0f)/g; 165 | 166 | export const optionTypes = { 167 | attachment: "attachment", 168 | string: "string", 169 | number: "number", 170 | role: "role", 171 | user: "user", 172 | channel: "channel", 173 | stringChoices: "stringchoices", 174 | numberChoices: "numberchoices" 175 | } 176 | 177 | export const contexts = { 178 | guild: 0, 179 | dm: 1, 180 | groupDm: 2 181 | } 182 | 183 | export const contextTypes = { 184 | message: "Message", 185 | user: "User" 186 | } -------------------------------------------------------------------------------- /utils/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | previewFeatures = ["fullTextSearchPostgres"] 7 | } 8 | 9 | datasource db { 10 | provider = "postgresql" 11 | url = env("DATABASE_URL") 12 | } 13 | 14 | model Settings { 15 | guildId String @id @unique 16 | embed Embed? 17 | language String 18 | } 19 | 20 | model GuildBlacklist { 21 | id String @id @unique 22 | reason String 23 | } 24 | 25 | model UserBlacklist { 26 | id String @id @unique 27 | reason String 28 | } 29 | 30 | model Embed { 31 | settings Settings? @relation(fields: [guildId], references: [guildId], onDelete: Cascade) 32 | guildId String @id @unique 33 | color String @default("#25fa6c") 34 | wrongcolor String @default("#e01e01") 35 | warncolor String @default("#ffa500") 36 | footertext String @default("Erry") 37 | footericon String @default("") 38 | } --------------------------------------------------------------------------------