diff --git a/.gitignore b/.gitignore index 4490afd..3321683 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ -errors/ \ No newline at end of file +errors/test-bot/ +test-bot/ \ No newline at end of file diff --git a/README.md b/README.md index 20b4212..f8ce13b 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ The interactive setup will guide you through: - **Hot Reload** - Changes apply instantly without restart - **Command Generator** - Scaffold new commands with `npm run generate` - **Command Manager** - Enable/disable commands with `npm run manage` +- **Middleware** - Pre-execution logic for commands ### 🔐 Advanced Controls - **Bot Permissions** - Automatic bot permission validation @@ -50,6 +51,7 @@ The interactive setup will guide you through: - **Role Requirements** - Restrict commands to specific roles - **Owner/Admin Only** - Special access controls - **Dev Mode** - Test commands in specific servers +- **Sharding** - Built-in scaling support (Optional) ### 📊 Dashboard & Monitoring - **Real-time Stats** - Monitor bot performance live @@ -64,6 +66,7 @@ The interactive setup will guide you through: - **MongoDB Integration** - Built-in database support with Mongoose - **Configurable Functions** - Advanced function options - **Error Recovery** - Graceful error handling +- **TypeScript Support** - Enhanced IntelliSense & type checking ## 📦 Installation @@ -105,6 +108,38 @@ module.exports = { }; ``` +## 🛡️ Middleware System + +DiscoBase includes a powerful middleware system that runs **before** any slash command is executed. Use it for global checks like blacklists, maintenance mode, or custom analytics. + +**Location:** `src/middleware/index.js` + +```javascript +module.exports = { + // Return true to continue, false to block the command + checkBlacklist: async (interaction) => { + const isBlacklisted = await db.blacklist.findOne({ userId: interaction.user.id }); + if (isBlacklisted) { + await interaction.reply({ content: 'You are blacklisted.', ephemeral: true }); + return false; // Blocks command + } + return true; // Continues execution + } +}; +``` + +## 📡 Sharding (Optional) + +For large bots (2,500+ servers), DiscoBase supports automatic sharding. You can enable this during setup. + +**Usage:** +```bash +node sharding.js +# or add "shard": "node sharding.js" to your package.json scripts +``` + +This will spawn multiple processes of your bot to handle high load, automatically managed by Discord.js `ShardingManager`. + ## 📅 Event Options Configure your event handlers with these options: diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..2eda13d --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "target": "ESNext", + "moduleResolution": "NodeNext", + "checkJs": true, + "allowJs": true, + "skipLibCheck": true, + "strict": false + }, + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/setup.mjs b/setup.mjs index 28fdfde..22d52b4 100644 --- a/setup.mjs +++ b/setup.mjs @@ -86,7 +86,7 @@ async function createProject() { process.exit(0); } - const useCurrentDir = await select({ + const useCurrentDirResult = await select({ message: 'Where would you like to create your project?', options: [ { value: 'new', label: '📁 Create in a new folder' }, @@ -94,11 +94,17 @@ async function createProject() { ] }); + if (isCancel(useCurrentDirResult)) { + cancel('Setup cancelled'); + process.exit(0); + } + const useCurrentDir = String(useCurrentDirResult); + let projectName; let destination; if (useCurrentDir === 'new') { - projectName = await text({ + const projectNameResult = await text({ message: 'What is your project name?', placeholder: 'my-discord-bot', validate: (value) => { @@ -107,12 +113,20 @@ async function createProject() { if (!/^[a-z0-9-_]+$/i.test(value)) return 'Use only letters, numbers, hyphens, and underscores'; } }); + if (isCancel(projectNameResult)) { + cancel('Setup cancelled'); + process.exit(0); + } + projectName = String(projectNameResult); destination = path.join(process.cwd(), projectName); } else { projectName = path.basename(process.cwd()); destination = process.cwd(); } + // Ensure projectName is a string + projectName = String(projectName); + // Check if directory exists and is not empty if (fs.existsSync(destination) && fs.readdirSync(destination).length > 0) { log.error(`Directory ${chalk.yellow(projectName)} already exists and is not empty!`); @@ -158,22 +172,46 @@ async function createProject() { process.exit(0); } + // Ask about Sharding (for both versions) + const includeSharding = await confirm({ + message: 'Enable Sharding System? (Optional)', + initialValue: false + }); + + if (isCancel(includeSharding)) { + cancel('Setup cancelled'); + process.exit(0); + } + // If old version selected, copy the template and handle dependencies if (versionChoice === 'old') { const s = spinner(); s.start('Copying full source code template...'); - const oldTemplatePath = path.join(__dirname, 'create-discobase'); + const oldTemplatePath = __dirname; // Copy everything except .git, node_modules, and setup files const itemsToCopy = fs.readdirSync(oldTemplatePath); for (const item of itemsToCopy) { + // Skip typical ignore files/folders if (item === '.git' || item === 'node_modules' || item === 'setup.mjs' || item === 'package-lock.json') { continue; } + // Skip the destination folder itself if it's inside the current directory const sourcePath = path.join(oldTemplatePath, item); + const resolvedSource = path.resolve(sourcePath); + const resolvedDest = path.resolve(destination); + + // Prevent copying if the source is the destination OR an ancestor of the destination + if (resolvedDest.startsWith(resolvedSource)) { + // Ensure it's a true parent (slash check) or exact match + if (resolvedDest === resolvedSource || resolvedDest[resolvedSource.length] === path.sep) { + continue; + } + } + const destPath = path.join(destination, item); if (fs.statSync(sourcePath).isDirectory()) { @@ -193,6 +231,14 @@ async function createProject() { } } + // Remove sharding.js if user doesn't want it + if (!includeSharding) { + const shardingPath = path.join(destination, 'sharding.js'); + if (fs.existsSync(shardingPath)) { + fs.unlinkSync(shardingPath); + } + } + // Install dependencies if requested if (installRequired) { const packages = ['discobase-core@latest', 'discord.js', 'nodemon', 'multer', 'figlet', 'micromatch', 'cli-progress', 'chalk@4', 'fs-extra', 'gradient-string', 'chokidar', 'axios', 'set-interval-async', 'boxen', '@clack/prompts']; @@ -322,6 +368,28 @@ async function createProject() { }; fs.writeFileSync(path.join(destination, 'discobase.json'), JSON.stringify(discobaseJson, null, 2)); + // Create sharding.js if requested + if (includeSharding) { + const shardingContent = `const { ShardingManager } = require('discord.js'); +const config = require('./config.json'); +const chalk = require('chalk'); + +const manager = new ShardingManager('./src/index.js', { + token: config.bot.token, + totalShards: 'auto' +}); + +manager.on('shardCreate', shard => { + console.log(chalk.blue(\`[SHARD] Launched shard \${shard.id}\`)); +}); + +manager.spawn().catch(error => { + console.error(chalk.red('[SHARDING ERROR] Failed to spawn shards:'), error); +}); +`; + fs.writeFileSync(path.join(destination, 'sharding.js'), shardingContent); + } + // Create example slash command const slashCommandContent = `//! This is a basic structure for a slash command in a discoBase using discord.js @@ -608,7 +676,7 @@ Visit [https://www.discobase.site](https://www.discobase.site) for full document note(successMessage, chalk.green.bold('Setup Complete')); outro(chalk.bold.cyan('✨ Happy coding! 🚀 Let\'s build something amazing!')); -} +} // Run the script createProject().catch(error => { diff --git a/sharding.js b/sharding.js new file mode 100644 index 0000000..6f8d83c --- /dev/null +++ b/sharding.js @@ -0,0 +1,16 @@ +const { ShardingManager } = require('discord.js'); +const config = require('./config.json'); +const chalk = require('chalk'); + +const manager = new ShardingManager('./src/index.js', { + token: config.bot.token, + totalShards: 'auto' +}); + +manager.on('shardCreate', shard => { + console.log(chalk.blue(`[SHARD] Launched shard ${shard.id}`)); +}); + +manager.spawn().catch(error => { + console.error(chalk.red('[SHARDING ERROR] Failed to spawn shards:'), error); +}); \ No newline at end of file diff --git a/src/events/handlers/interactionCreate.js b/src/events/handlers/interactionCreate.js index 794ac5a..aadbc00 100644 --- a/src/events/handlers/interactionCreate.js +++ b/src/events/handlers/interactionCreate.js @@ -4,6 +4,12 @@ const config = require('../../../config.json'); const path = require('path'); const fs = require('fs'); const mongoose = require('mongoose'); +let middleware; +try { + middleware = require('../../middleware/index.js'); +} catch (e) { + middleware = {}; +} const errorsDir = path.join(__dirname, '../../../errors'); @@ -167,6 +173,16 @@ module.exports = { return; } + // Execute Middleware + if (middleware) { + for (const [name, fn] of Object.entries(middleware)) { + if (typeof fn === 'function') { + const continueExecution = await fn(interaction); + if (continueExecution === false) return; + } + } + } + // if (!interaction.deferred && !interaction.replied) { // await interaction.deferReply({ flags: MessageFlags.Ephemeral }).catch(() => {}); // } diff --git a/src/functions/handlers/functionHandler.js b/src/functions/handlers/functionHandler.js index 7d8d431..b324a95 100644 --- a/src/functions/handlers/functionHandler.js +++ b/src/functions/handlers/functionHandler.js @@ -15,7 +15,9 @@ const getAllFunctionFiles = (dir) => { const stat = fs.statSync(filePath); if (stat && stat.isDirectory()) { - results = results.concat(getAllFunctionFiles(filePath)); + if (file !== 'handlers') { + results = results.concat(getAllFunctionFiles(filePath)); + } } else if (file.endsWith('.js')) { results.push(filePath); } diff --git a/src/functions/handlers/handelEvents.js b/src/functions/handlers/handelEvents.js index 4f42e61..d986c03 100644 --- a/src/functions/handlers/handelEvents.js +++ b/src/functions/handlers/handelEvents.js @@ -147,6 +147,7 @@ const checkPermissions = async (event, interaction) => { const eventsHandler = async (client, eventsPath) => { client.events = new Collection(); client.components = new Collection(); + client.activeEventListeners = new Map(); const getFilesRecursively = (dir) => { let files = []; @@ -260,6 +261,9 @@ const eventsHandler = async (client, eventsPath) => { client.on(event.name, wrappedExecute); } + // Store the listener for safe removal + client.activeEventListeners.set(file, { name: event.name, listener: wrappedExecute }); + console.log( `${getTimestamp()} ` + chalk.green.bold('SUCCESS: ') + @@ -306,21 +310,24 @@ const eventsHandler = async (client, eventsPath) => { const unloadEvent = (file) => { try { - const event = require(file); + const listenerData = client.activeEventListeners.get(file); + + if (listenerData) { + const { name, listener } = listenerData; + + // Safely remove the specific listener + client.removeListener(name, listener); + + // Clean up tracking maps + client.activeEventListeners.delete(file); + if (client.events.has(name)) { + client.events.delete(name); + } - if (event.name && client.events.has(event.name)) { - client.removeAllListeners(event.name); - client.events.delete(event.name); console.log( `${getTimestamp()} ` + chalk.blue.bold('UNLOAD: ') + - `Unloaded event: ${chalk.cyan.bold(event.name)}` - ); - } else { - console.warn( - `${getTimestamp()} ` + - chalk.yellow.bold('WARNING: ') + - `Event/Component "${chalk.red(getShortPath(file))}" not found in client collections.` + `Unloaded event: ${chalk.cyan.bold(name)}` ); } } catch (error) { diff --git a/src/middleware/index.js b/src/middleware/index.js new file mode 100644 index 0000000..b03e23b --- /dev/null +++ b/src/middleware/index.js @@ -0,0 +1,32 @@ +/** + * Middleware functions for DiscoBase + * Returns true to continue execution, false to stop. + */ +module.exports = { + /** + * Example: Check if the user is blacklisted + * @param {import('discord.js').Interaction} interaction + */ + checkBlacklist: async (interaction) => { + // Implement your blacklist logic here + // const isBlacklisted = await db.blacklist.findOne({ userId: interaction.user.id }); + // if (isBlacklisted) { + // await interaction.reply({ content: 'You are blacklisted.', ephemeral: true }); + // return false; + // } + return true; + }, + + /** + * Example: Global maintenance mode + * @param {import('discord.js').Interaction} interaction + */ + maintenanceMode: async (interaction) => { + // const maintenance = false; + // if (maintenance) { + // await interaction.reply({ content: 'Maintenance mode is on.', ephemeral: true }); + // return false; + // } + return true; + } +}; \ No newline at end of file