From f44c1aef9457cb89357a48fff5b4c8e9fd143e01 Mon Sep 17 00:00:00 2001 From: Pilot2254 Date: Thu, 7 Nov 2024 20:50:49 +0100 Subject: [PATCH] Update 2.0 --- README.md | 221 ++++++++++++++++++++++++++------- commands/avatar.js | 8 -- commands/echo.js | 10 -- commands/fun/choose.js | 27 ++++ commands/fun/roll.js | 20 +++ commands/general/help.js | 45 +++++++ commands/general/ping.js | 11 ++ commands/help.js | 36 ------ commands/moderation/kick.js | 34 +++++ commands/ping.js | 7 -- commands/roll.js | 12 -- commands/serverinfo.js | 8 -- commands/userinfo.js | 8 -- commands/utility/avatar.js | 16 +++ commands/utility/echo.js | 16 +++ commands/utility/serverinfo.js | 20 +++ commands/utility/userinfo.js | 24 ++++ config.json | 25 +++- deploy-commands.js | 39 ++++++ index.js | 85 +++++++++---- logger.js | 64 ++++++++++ utils/pagination.js | 54 ++++++++ 22 files changed, 625 insertions(+), 165 deletions(-) delete mode 100644 commands/avatar.js delete mode 100644 commands/echo.js create mode 100644 commands/fun/choose.js create mode 100644 commands/fun/roll.js create mode 100644 commands/general/help.js create mode 100644 commands/general/ping.js delete mode 100644 commands/help.js create mode 100644 commands/moderation/kick.js delete mode 100644 commands/ping.js delete mode 100644 commands/roll.js delete mode 100644 commands/serverinfo.js delete mode 100644 commands/userinfo.js create mode 100644 commands/utility/avatar.js create mode 100644 commands/utility/echo.js create mode 100644 commands/utility/serverinfo.js create mode 100644 commands/utility/userinfo.js create mode 100644 deploy-commands.js create mode 100644 logger.js create mode 100644 utils/pagination.js diff --git a/README.md b/README.md index ab151c7..016d1f5 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,202 @@ -# DiscordBot +# Advanced Discord Bot# Advanced Discord Bot -[![GitHub release](https://img.shields.io/github/release/RedFox-Studios/DiscordBot.svg)](https://github.com/RedFox-Studios/DiscordBot/releases) -[![GitHub issues](https://img.shields.io/github/issues/RedFox-Studios/DiscordBot.svg)](https://github.com/RedFox-Studios/DiscordBot/issues) -[![GitHub license](https://img.shields.io/github/license/RedFox-Studios/DiscordBot.svg)](./LICENSE) - -Welcome to **DiscordBot**, a customizable Discord bot developed by RedFox Studios. Designed to be flexible and efficient, this bot provides an array of commands that enhance your Discord server experience. +A feature-rich Discord bot with slash commands, error handling, cooldowns, permissions, pagination, and more. ## Table of Contents + - [Features](#features) -- [Installation](#installation) +- [Setup](#setup) +- [File Structure](#file-structure) - [Configuration](#configuration) -- [Usage](#usage) +- [Command System](#command-system) +- [Logging System](#logging-system) +- [Cooldowns and Permissions](#cooldowns-and-permissions) +- [Pagination Utility](#pagination-utility) +- [Adding New Commands](#adding-new-commands) +- [Deployment](#deployment) - [Contributing](#contributing) -- [License](#license) ## Features -- πŸ› οΈ Easily extendable commands -- βš™οΈ Configurable settings via `config.json` -- πŸ“œ Modular command structure -## Installation -To set up and run DiscordBot on your local environment, follow these steps: +- Slash command support +- Error handling and graceful degradation +- Cooldown system +- Permission-based command access +- Pagination for long responses +- Interactive help menu +- Moderation tools +- Logging system + +## Setup + +1. Clone the repository +2. Install dependencies: + +``` +npm install +``` +3. Copy `config.example.json` to `config.json` and fill in your bot token, client ID, and guild ID3. Copy `config.example.json` to `config.json` and fill in your bot token, client ID, and guild ID +4. Register slash commands: -1. **Clone the repository:** - ```bash - git clone https://github.com/RedFox-Studios/DiscordBot.git - cd DiscordBot - ``` +``` +node deploy-commands.js +``` -2. **Install dependencies:** - ```bash - npm install - ``` +5. Start the bot: -3. **Configure the bot:** - Edit `config.json` with your bot token and other configuration details. +``` +node index.js +``` -4. **Start the bot:** - ```bash - node index.js - ``` +## File Structure + +``` +discord-bot/ +β”‚ +β”œβ”€β”€ commands/ +β”‚ β”œβ”€β”€ fun/ +β”‚ β”‚ └── choose.js +β”‚ β”œβ”€β”€ general/ +β”‚ β”‚ └── help.js +β”‚ └── moderation/ +β”‚ └── kick.js +β”‚ +β”œβ”€β”€ logs/ +β”‚ β”œβ”€β”€ errors.log +β”‚ β”œβ”€β”€ servers.log +β”‚ β”œβ”€β”€ commands.log +β”‚ └── server_list.log +β”‚ +β”œβ”€β”€ utils/ +β”‚ └── pagination.js +β”‚ +β”œβ”€β”€ config.json +β”œβ”€β”€ deploy-commands.js +β”œβ”€β”€ index.js +β”œβ”€β”€ logger.js +└── README.md +``` ## Configuration -Edit the `config.json` file to customize bot settings, including the Discord bot token and command prefixes. Make sure to keep this file secure. -Example `config.json`: +Edit `config.json` to customize the bot's behavior: + ```json { - "token": "BOT_TOKEN", - "prefix": "!", - "clientId": "CLIENT_ID" + "token": "YOUR_BOT_TOKEN", + "clientId": "YOUR_CLIENT_ID", + "guildId": "YOUR_GUILD_ID", + "logging": { + "errors": true, + "servers": true, + "commands": true + }, + "cooldowns": { + "general": 3, + "moderation": 5, + "fun": 2 + }, + "permissions": { + "general": "SEND_MESSAGES", + "moderation": "MODERATE_MEMBERS", + "fun": "SEND_MESSAGES" + } } ``` -## Usage -The bot comes with a set of pre-configured commands found in the `commands/` directory. You can add or modify commands as needed. +## Command System -## Contributing -We welcome contributions! Please fork the repository and create a pull request with your changes. Be sure to follow coding standards and add documentation as necessary. +Commands are organized into categories (folders) within the `commands/` directory. Each command is a separate file that exports an object with the following structure: -## License -This project is licensed under the terms of the [MIT License](./LICENSE). +```javascript + module.exports = {module.exports = { + category: 'category_name', + data: new SlashCommandBuilder() + .setName('command_name') + .setDescription('Command description'), + async execute(interaction) { + // Command logic here + }, +}; +``` + +## Logging System + +The bot automatically creates a `logs/` directory with the following log files: + +- `errors.log`: Any errors encountered during bot operation +- `servers.log`: Server join/leave events +- `commands.log`: Command usage +- `server_list.log`: Up-to-date list of servers the bot is in + + +Logging can be configured in `config.json`. ---- +## Cooldowns and Permissions -### requirements.txt -If it’s a **Node.js bot**, you won’t need `requirements.txt` (that’s for Python). Instead, use `package.json` to list dependencies like `"discord.js"` for Node.js bots. If you don’t have `package.json` yet, run this: +Cooldowns and permissions are defined in `config.json` for each command category. The bot automatically applies these settings to all commands within a category. + +## Pagination Utility + +The `utils/pagination.js` file provides a utility for paginating long responses. Use it in your commands like this: + +```javascript + const pagination = require('../../utils/pagination');const pagination = require('../../utils/pagination'); + +// ... in your command's execute function: +const pages = [ + new EmbedBuilder().setDescription('Page 1 content'), + new EmbedBuilder().setDescription('Page 2 content'), + // ... more pages +]; + +pagination(interaction, pages); +``` + +## Adding New Commands + +1. Create a new file in the appropriate category folder within `commands/` +2. Use the following template: + + +```javascript + const { SlashCommandBuilder } = require('discord.js');const { SlashCommandBuilder } = require('discord.js'); + +module.exports = { + category: 'category_name', + data: new SlashCommandBuilder() + .setName('command_name') + .setDescription('Command description'), + async execute(interaction) { + // Command logic here + }, +}; -```bash -npm init -y -npm install discord.js dotenv ``` -This creates `package.json` with your dependencies (which acts like `requirements.txt` in Python). +3. Run `node deploy-commands.js` to register the new command with Discord + + +## Deployment + +1. Ensure all your changes are committed and pushed to your repository +2. Set up a hosting platform (e.g., Heroku, DigitalOcean, or a VPS) +3. Configure environment variables for your bot token and other sensitive information +4. Deploy your bot to the hosting platform +5. Ensure the bot starts correctly and can connect to Discord + + +## Contributing + +1. Fork the repository +2. Create a new branch: `git checkout -b feature-name` +3. Make your changes and commit them: `git commit -m 'Add some feature'` +4. Push to the branch: `git push origin feature-name` +5. Submit a pull request + + +Please ensure your code follows the existing style and includes appropriate documentation. + +## License + +This project is licensed under the MIT License. \ No newline at end of file diff --git a/commands/avatar.js b/commands/avatar.js deleted file mode 100644 index dd974d6..0000000 --- a/commands/avatar.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - name: 'avatar', - description: 'Display user avatar', - execute(message, args) { - const user = message.mentions.users.first() || message.author; - message.channel.send(`${user.username}'s avatar: ${user.displayAvatarURL({ dynamic: true })}`); - }, -}; \ No newline at end of file diff --git a/commands/echo.js b/commands/echo.js deleted file mode 100644 index b763c3b..0000000 --- a/commands/echo.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - name: 'echo', - description: 'Echoes a message.', - execute(message, args) { - if (!args.length) { - return message.reply('You need to provide a message to echo!'); - } - message.channel.send(args.join(' ')); - }, - }; \ No newline at end of file diff --git a/commands/fun/choose.js b/commands/fun/choose.js new file mode 100644 index 0000000..bcaa179 --- /dev/null +++ b/commands/fun/choose.js @@ -0,0 +1,27 @@ +const { SlashCommandBuilder } = require('discord.js'); + +module.exports = { + category: 'fun', + data: new SlashCommandBuilder() + .setName('choose') + .setDescription('Choose a number between two values') + .addIntegerOption(option => + option.setName('min') + .setDescription('The minimum number') + .setRequired(true)) + .addIntegerOption(option => + option.setName('max') + .setDescription('The maximum number') + .setRequired(true)), + async execute(interaction) { + const min = interaction.options.getInteger('min'); + const max = interaction.options.getInteger('max'); + + if (min >= max) { + return interaction.reply('The minimum number must be less than the maximum number.'); + } + + const result = Math.floor(Math.random() * (max - min + 1)) + min; + await interaction.reply(`I choose... ${result}!`); + }, +}; \ No newline at end of file diff --git a/commands/fun/roll.js b/commands/fun/roll.js new file mode 100644 index 0000000..0e320f7 --- /dev/null +++ b/commands/fun/roll.js @@ -0,0 +1,20 @@ +const { SlashCommandBuilder } = require('discord.js'); + +module.exports = { + category: 'fun', + data: new SlashCommandBuilder() + .setName('roll') + .setDescription('Roll a die') + .addIntegerOption(option => + option.setName('sides') + .setDescription('The number of sides on the die') + .setRequired(false)), + async execute(interaction) { + const sides = interaction.options.getInteger('sides') || 6; + if (sides < 2) { + return interaction.reply('The die must have at least 2 sides!'); + } + const result = Math.floor(Math.random() * sides) + 1; + await interaction.reply(`You rolled a ${result} on a ${sides}-sided die!`); + }, +}; \ No newline at end of file diff --git a/commands/general/help.js b/commands/general/help.js new file mode 100644 index 0000000..3c029db --- /dev/null +++ b/commands/general/help.js @@ -0,0 +1,45 @@ +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); +const pagination = require('../../utils/pagination'); + +module.exports = { + category: 'general', + data: new SlashCommandBuilder() + .setName('help') + .setDescription('List all commands or info about a specific command') + .addStringOption(option => + option.setName('command') + .setDescription('The specific command to see info about') + .setRequired(false)), + async execute(interaction) { + const { commands } = interaction.client; + const { options } = interaction; + + const commandName = options.getString('command'); + if (commandName) { + const command = commands.get(commandName); + if (!command) { + return interaction.reply(`No command with name \`${commandName}\` found.`); + } + + const embed = new EmbedBuilder() + .setTitle(`Command: ${command.data.name}`) + .setDescription(command.data.description) + .addFields( + { name: 'Category', value: command.category }, + { name: 'Usage', value: command.data.options ? command.data.options.map(option => `${option.name}: ${option.description}`).join('\n') : 'No options' } + ); + + return interaction.reply({ embeds: [embed] }); + } + + const categories = [...new Set(commands.map(command => command.category))]; + const pages = categories.map(category => { + const categoryCommands = commands.filter(command => command.category === category); + return new EmbedBuilder() + .setTitle(`${category.charAt(0).toUpperCase() + category.slice(1)} Commands`) + .setDescription(categoryCommands.map(command => `**${command.data.name}**: ${command.data.description}`).join('\n')); + }); + + pagination(interaction, pages); + }, +}; \ No newline at end of file diff --git a/commands/general/ping.js b/commands/general/ping.js new file mode 100644 index 0000000..f22711e --- /dev/null +++ b/commands/general/ping.js @@ -0,0 +1,11 @@ +const { SlashCommandBuilder } = require('discord.js'); + +module.exports = { + category: 'general', + data: new SlashCommandBuilder() + .setName('ping') + .setDescription('Replies with Pong!'), + async execute(interaction) { + await interaction.reply('Pong!'); + }, +}; \ No newline at end of file diff --git a/commands/help.js b/commands/help.js deleted file mode 100644 index 7fddf4b..0000000 --- a/commands/help.js +++ /dev/null @@ -1,36 +0,0 @@ -module.exports = { - name: 'help', - description: 'List all commands or info about a specific command.', - execute(message, args) { - const { commands } = message.client; - const data = []; - - if (!args.length) { - data.push('Here\'s a list of all my commands:'); - data.push(commands.map(command => command.name).join(', ')); - data.push(`\nYou can send \`!help [command name]\` to get info on a specific command!`); - - return message.author.send(data.join('\n')) - .then(() => { - if (message.channel.type === 'dm') return; - message.reply('I\'ve sent you a DM with all my commands!'); - }) - .catch(error => { - console.error(`Could not send help DM to ${message.author.tag}.\n`, error); - message.reply('It seems like I can\'t DM you! Do you have DMs disabled?'); - }); - } - - const name = args[0].toLowerCase(); - const command = commands.get(name); - - if (!command) { - return message.reply('That\'s not a valid command!'); - } - - data.push(`**Name:** ${command.name}`); - if (command.description) data.push(`**Description:** ${command.description}`); - - message.channel.send(data.join('\n')); - }, - }; \ No newline at end of file diff --git a/commands/moderation/kick.js b/commands/moderation/kick.js new file mode 100644 index 0000000..8d3abe1 --- /dev/null +++ b/commands/moderation/kick.js @@ -0,0 +1,34 @@ +const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js'); + +module.exports = { + category: 'moderation', + data: new SlashCommandBuilder() + .setName('kick') + .setDescription('Kick a user from the server') + .addUserOption(option => + option.setName('target') + .setDescription('The user to kick') + .setRequired(true)) + .addStringOption(option => + option.setName('reason') + .setDescription('The reason for kicking')) + .setDefaultMemberPermissions(PermissionFlagsBits.KickMembers), + async execute(interaction) { + const target = interaction.options.getUser('target'); + const reason = interaction.options.getString('reason') ?? 'No reason provided'; + + const member = await interaction.guild.members.fetch(target.id).catch(console.error); + + if (!member) { + return interaction.reply({ content: 'That user isn\'t in this guild!', ephemeral: true }); + } + + if (!member.kickable) { + return interaction.reply({ content: 'I cannot kick this user! Do they have a higher role?', ephemeral: true }); + } + + await member.kick(reason); + + await interaction.reply(`Successfully kicked ${target.tag} for reason: ${reason}`); + }, +}; \ No newline at end of file diff --git a/commands/ping.js b/commands/ping.js deleted file mode 100644 index 73f6120..0000000 --- a/commands/ping.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - name: 'ping', - description: 'Ping!', - execute(message, args) { - message.channel.send('Pong!'); - }, - }; \ No newline at end of file diff --git a/commands/roll.js b/commands/roll.js deleted file mode 100644 index 8c3739a..0000000 --- a/commands/roll.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = { - name: 'roll', - description: 'Roll a die. Usage: !roll ', - execute(message, args) { - const sides = parseInt(args[0]) || 6; - if (sides < 2) { - return message.reply('The die must have at least 2 sides!'); - } - const result = Math.floor(Math.random() * sides) + 1; - message.reply(`You rolled a ${result} on a ${sides}-sided die!`); - }, - }; \ No newline at end of file diff --git a/commands/serverinfo.js b/commands/serverinfo.js deleted file mode 100644 index 2f071f2..0000000 --- a/commands/serverinfo.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - name: 'serverinfo', - description: 'Displays server info', - execute(message, args) { - const { name, memberCount, owner } = message.guild; - message.channel.send(`Server Name: ${name}\nTotal Members: ${memberCount}\nServer Owner: ${owner.user.tag}`); - }, -}; \ No newline at end of file diff --git a/commands/userinfo.js b/commands/userinfo.js deleted file mode 100644 index 3775d78..0000000 --- a/commands/userinfo.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - name: 'userinfo', - description: 'Displays info about the user', - execute(message, args) { - const user = message.author; - message.channel.send(`Username: ${user.tag}\nID: ${user.id}\nCreated At: ${user.createdAt}`); - }, -}; diff --git a/commands/utility/avatar.js b/commands/utility/avatar.js new file mode 100644 index 0000000..77dc1aa --- /dev/null +++ b/commands/utility/avatar.js @@ -0,0 +1,16 @@ +const { SlashCommandBuilder } = require('discord.js'); + +module.exports = { + category: 'utility', + data: new SlashCommandBuilder() + .setName('avatar') + .setDescription('Display user avatar') + .addUserOption(option => + option.setName('user') + .setDescription('The user whose avatar to show') + .setRequired(false)), + async execute(interaction) { + const user = interaction.options.getUser('user') || interaction.user; + await interaction.reply(`${user.username}'s avatar: ${user.displayAvatarURL({ dynamic: true })}`); + }, +}; \ No newline at end of file diff --git a/commands/utility/echo.js b/commands/utility/echo.js new file mode 100644 index 0000000..9a0897a --- /dev/null +++ b/commands/utility/echo.js @@ -0,0 +1,16 @@ +const { SlashCommandBuilder } = require('discord.js'); + +module.exports = { + category: 'utility', + data: new SlashCommandBuilder() + .setName('echo') + .setDescription('Echoes a message') + .addStringOption(option => + option.setName('message') + .setDescription('The message to echo') + .setRequired(true)), + async execute(interaction) { + const message = interaction.options.getString('message'); + await interaction.reply(message); + }, +}; \ No newline at end of file diff --git a/commands/utility/serverinfo.js b/commands/utility/serverinfo.js new file mode 100644 index 0000000..c5a7396 --- /dev/null +++ b/commands/utility/serverinfo.js @@ -0,0 +1,20 @@ +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); + +module.exports = { + category: 'utility', + data: new SlashCommandBuilder() + .setName('serverinfo') + .setDescription('Displays server info'), + async execute(interaction) { + const { guild } = interaction; + const embed = new EmbedBuilder() + .setTitle(`Server Info - ${guild.name}`) + .setThumbnail(guild.iconURL({ dynamic: true })) + .addFields( + { name: 'Server Name', value: guild.name }, + { name: 'Total Members', value: guild.memberCount.toString() }, + { name: 'Server Owner', value: (await guild.fetchOwner()).user.tag } + ); + await interaction.reply({ embeds: [embed] }); + }, +}; \ No newline at end of file diff --git a/commands/utility/userinfo.js b/commands/utility/userinfo.js new file mode 100644 index 0000000..b71bf4f --- /dev/null +++ b/commands/utility/userinfo.js @@ -0,0 +1,24 @@ +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); + +module.exports = { + category: 'utility', + data: new SlashCommandBuilder() + .setName('userinfo') + .setDescription('Displays info about the user') + .addUserOption(option => + option.setName('user') + .setDescription('The user to get info about') + .setRequired(false)), + async execute(interaction) { + const user = interaction.options.getUser('user') || interaction.user; + const embed = new EmbedBuilder() + .setTitle(`User Info - ${user.tag}`) + .setThumbnail(user.displayAvatarURL({ dynamic: true })) + .addFields( + { name: 'Username', value: user.username }, + { name: 'ID', value: user.id }, + { name: 'Created At', value: user.createdAt.toDateString() } + ); + await interaction.reply({ embeds: [embed] }); + }, +}; \ No newline at end of file diff --git a/config.json b/config.json index 86aec32..6abc3a4 100644 --- a/config.json +++ b/config.json @@ -1,5 +1,22 @@ { - "token": "BOT_TOKEN", - "prefix": "!", - "clientId": "CLIENT_ID" - } \ No newline at end of file + "token": "YOUR_BOT_TOKEN", + "clientId": "YOUR_CLIENT_ID", + "guildId": "YOUR_GUILD_ID", + "logging": { + "errors": true, + "servers": true, + "commands": true + }, + "cooldowns": { + "general": 3, + "moderation": 5, + "fun": 2, + "utility": 3 + }, + "permissions": { + "general": "SEND_MESSAGES", + "moderation": "MODERATE_MEMBERS", + "fun": "SEND_MESSAGES", + "utility": "SEND_MESSAGES" + } +} \ No newline at end of file diff --git a/deploy-commands.js b/deploy-commands.js new file mode 100644 index 0000000..3e571c7 --- /dev/null +++ b/deploy-commands.js @@ -0,0 +1,39 @@ +const { REST, Routes } = require('discord.js'); +const fs = require('node:fs'); +const path = require('node:path'); +const { clientId, guildId, token } = require('./config.json'); + +const commands = []; +const foldersPath = path.join(__dirname, 'commands'); +const commandFolders = fs.readdirSync(foldersPath); + +for (const folder of commandFolders) { + const commandsPath = path.join(foldersPath, folder); + const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js')); + for (const file of commandFiles) { + const filePath = path.join(commandsPath, file); + const command = require(filePath); + if ('data' in command && 'execute' in command) { + commands.push(command.data.toJSON()); + } else { + console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`); + } + } +} + +const rest = new REST().setToken(token); + +(async () => { + try { + console.log(`Started refreshing ${commands.length} application (/) commands.`); + + const data = await rest.put( + Routes.applicationGuildCommands(clientId, guildId), + { body: commands }, + ); + + console.log(`Successfully reloaded ${data.length} application (/) commands.`); + } catch (error) { + console.error(error); + } +})(); \ No newline at end of file diff --git a/index.js b/index.js index da6be27..f93412e 100644 --- a/index.js +++ b/index.js @@ -1,42 +1,77 @@ -const fs = require('fs'); -const { Client, Collection, GatewayIntentBits } = require('discord.js'); -const { token, prefix } = require('./config.json'); - -const client = new Client({ - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.MessageContent, - ], -}); +const fs = require('node:fs'); +const path = require('node:path'); +const { Client, Collection, Events, GatewayIntentBits } = require('discord.js'); +const { token } = require('./config.json'); +const Logger = require('./logger'); + +const client = new Client({ intents: [GatewayIntentBits.Guilds] }); client.commands = new Collection(); -const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js')); +client.cooldowns = new Collection(); +const foldersPath = path.join(__dirname, 'commands'); +const commandFolders = fs.readdirSync(foldersPath); -for (const file of commandFiles) { - const command = require(`./commands/${file}`); - client.commands.set(command.name, command); +for (const folder of commandFolders) { + const commandsPath = path.join(foldersPath, folder); + const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js')); + for (const file of commandFiles) { + const filePath = path.join(commandsPath, file); + const command = require(filePath); + if ('data' in command && 'execute' in command) { + client.commands.set(command.data.name, command); + } else { + console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`); + } + } } -client.once('ready', () => { - console.log('Bot is ready!'); +const logger = new Logger(client); + +client.once(Events.ClientReady, () => { + console.log('Ready!'); + logger.updateServerList(); }); -client.on('messageCreate', message => { - if (!message.content.startsWith(prefix) || message.author.bot) return; +client.on(Events.InteractionCreate, async interaction => { + if (!interaction.isChatInputCommand()) return; + + const command = client.commands.get(interaction.commandName); + + if (!command) return; - const args = message.content.slice(prefix.length).trim().split(/ +/); - const commandName = args.shift().toLowerCase(); + const { cooldowns } = require('./config.json'); + const { permissions } = require('./config.json'); - if (!client.commands.has(commandName)) return; + if (!interaction.member.permissions.has(permissions[command.category])) { + return interaction.reply({ content: 'You do not have permission to use this command.', ephemeral: true }); + } + + const now = Date.now(); + const cooldownAmount = (cooldowns[command.category] || 3) * 1000; + + if (client.cooldowns.has(interaction.user.id)) { + const expirationTime = client.cooldowns.get(interaction.user.id) + cooldownAmount; + + if (now < expirationTime) { + const timeLeft = (expirationTime - now) / 1000; + return interaction.reply({ content: `Please wait ${timeLeft.toFixed(1)} more second(s) before reusing this command.`, ephemeral: true }); + } + } - const command = client.commands.get(commandName); + client.cooldowns.set(interaction.user.id, now); + setTimeout(() => client.cooldowns.delete(interaction.user.id), cooldownAmount); try { - command.execute(message, args); + await command.execute(interaction); + logger.log('commands', `User ${interaction.user.tag} executed command: ${interaction.commandName}`); } catch (error) { console.error(error); - message.reply('There was an error executing that command!'); + logger.logError(error); + if (interaction.replied || interaction.deferred) { + await interaction.followUp({ content: 'There was an error while executing this command!', ephemeral: true }); + } else { + await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true }); + } } }); diff --git a/logger.js b/logger.js new file mode 100644 index 0000000..9673d1d --- /dev/null +++ b/logger.js @@ -0,0 +1,64 @@ +const fs = require('fs'); +const path = require('path'); + +class Logger { + constructor(config) { + this.config = config; + this.logsDir = path.join(__dirname, 'logs'); + this.logFiles = ['errors', 'servers', 'commands', 'server_list']; + + this.initializeLogsFolder(); + } + + initializeLogsFolder() { + // Create logs folder if it doesn't exist + if (!fs.existsSync(this.logsDir)) { + fs.mkdirSync(this.logsDir); + console.log('Created logs folder'); + } + + // Create log files if they don't exist + this.logFiles.forEach(file => { + const filePath = path.join(this.logsDir, `${file}.log`); + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, ''); + console.log(`Created ${file}.log`); + } + }); + } + + log(type, message) { + if (this.config.logging[type]) { + const logFile = path.join(this.logsDir, `${type}.log`); + const timestamp = new Date().toISOString(); + const logMessage = `[${timestamp}] ${message}\n`; + + fs.appendFileSync(logFile, logMessage); + } + } + + logError(error) { + this.log('errors', `Error: ${error.message}\nStack: ${error.stack}`); + } + + logServerJoin(guild) { + this.log('servers', `Bot joined server: ${guild.name} (ID: ${guild.id})`); + this.updateServerList(); + } + + logServerLeave(guild) { + this.log('servers', `Bot left server: ${guild.name} (ID: ${guild.id})`); + this.updateServerList(); + } + + updateServerList() { + if (this.config.logging.servers) { + const serverListFile = path.join(this.logsDir, 'server_list.log'); + const servers = Array.from(this.config.client.guilds.cache.values()); + const serverList = servers.map(guild => `${guild.name} (ID: ${guild.id})`).join('\n'); + fs.writeFileSync(serverListFile, serverList); + } + } +} + +module.exports = Logger; \ No newline at end of file diff --git a/utils/pagination.js b/utils/pagination.js new file mode 100644 index 0000000..8204ef8 --- /dev/null +++ b/utils/pagination.js @@ -0,0 +1,54 @@ +const { ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); + +module.exports = async (interaction, pages, timeout = 120000) => { + if (!pages || !pages.length) return; + + const row = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId('first') + .setLabel('<<') + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId('prev') + .setLabel('<') + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId('next') + .setLabel('>') + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId('last') + .setLabel('>>') + .setStyle(ButtonStyle.Primary) + ); + + let currentPage = 0; + + const curPage = await interaction.reply({ + embeds: [pages[currentPage].setFooter({ text: `Page ${currentPage + 1} / ${pages.length}` })], + components: [row], + fetchReply: true + }); + + const filter = i => i.customId === 'first' || i.customId === 'prev' || i.customId === 'next' || i.customId === 'last'; + const collector = curPage.createMessageComponentCollector({ filter, time: timeout }); + + collector.on('collect', async i => { + if (i.customId === 'first') currentPage = 0; + if (i.customId === 'prev') currentPage = currentPage > 0 ? --currentPage : pages.length - 1; + if (i.customId === 'next') currentPage = currentPage + 1 < pages.length ? ++currentPage : 0; + if (i.customId === 'last') currentPage = pages.length - 1; + + await i.update({ + embeds: [pages[currentPage].setFooter({ text: `Page ${currentPage + 1} / ${pages.length}` })], + components: [row] + }); + }); + + collector.on('end', () => { + curPage.edit({ components: [] }); + }); + + return curPage; +}; \ No newline at end of file