Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules/
errors/
errors/test-bot/
test-bot/
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
12 changes: 12 additions & 0 deletions jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"module": "NodeNext",
"target": "ESNext",
"moduleResolution": "NodeNext",
"checkJs": true,
"allowJs": true,
"skipLibCheck": true,
"strict": false
},
"exclude": ["node_modules"]
}
76 changes: 72 additions & 4 deletions setup.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -86,19 +86,25 @@ 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' },
{ value: 'current', label: '📍 Use current directory' }
]
});

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) => {
Expand All @@ -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!`);
Expand Down Expand Up @@ -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()) {
Expand All @@ -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'];
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 => {
Expand Down
16 changes: 16 additions & 0 deletions sharding.js
Original file line number Diff line number Diff line change
@@ -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);
});
16 changes: 16 additions & 0 deletions src/events/handlers/interactionCreate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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(() => {});
// }
Expand Down
4 changes: 3 additions & 1 deletion src/functions/handlers/functionHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
29 changes: 18 additions & 11 deletions src/functions/handlers/handelEvents.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand Down Expand Up @@ -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: ') +
Expand Down Expand Up @@ -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) {
Expand Down
32 changes: 32 additions & 0 deletions src/middleware/index.js
Original file line number Diff line number Diff line change
@@ -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;
}
};