From 6336053d41ebf12e9757775aa21bf9964cefc1fd Mon Sep 17 00:00:00 2001 From: ScriptSorcerer20 Date: Fri, 24 Jan 2025 14:23:24 +0100 Subject: [PATCH 01/10] Create README.md a file to keep track of my goals =) --- README.md | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..cbb2413 --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +Im going to make a To-do List for the features i want to implement for my Bot. + +### **To-Do List for My Discord Bot** + +#### **Core Features** +(I will try to make these tools optional, because alot of bots already feature them) + +1. **Basic Commands** + - [ ] Add a `!help` command to display a list of available commands. + - [ ] Implement a `!ping` command to check bot responsiveness. + - [ ] Add a `!about` command to describe the bot's purpose. + +2. **Welcome and Goodbye System** + - [ ] Create a system to welcome new users to the server with a customizable message. + - [ ] Implement a goodbye message when users leave the server. + +3. **Role Management** + - [ ] Set up a self-assignable role system (e.g., using reaction roles). + - [ ] Allow admins to assign/remove roles with commands. + +4. **Moderation Tools** + - [ ] Add commands to mute, kick, and ban users. + - [ ] Create an auto-moderation feature to delete spam or inappropriate content. + - [ ] Implement a warning system with a strike counter. + +--- + +#### **Unique Features** +1. **Currency System** + - [ ] Design a virtual currency system with basic commands: + - [ ] `!balance` to check currency balance. + - [ ] `!earn` to earn daily or periodic rewards. + - [ ] `!transfer @user [amount]` to send currency to another user. + - [ ] Link the currency system to role management: + - [ ] Allow users to purchase roles using the currency with a command (e.g., `!buyrole [role name]`). + +2. **Datetime Converter** + - [ ] Create a command to convert timezones: + - [ ] `!convert [time] [timezone] to [timezone]` to handle conversions. + - [ ] Support both 12-hour and 24-hour formats. + - [ ] Include a command to display server-specific or user-specific timezones. + +3. **Minigames** + - [ ] Implement a Quiz Game: + - [ ] Add a `!quiz` command to start the game. + - [ ] Provide multiple-choice or true/false questions. + - [ ] Award virtual currency for correct answers. + - [ ] Add a simple number-guessing game (e.g., `!guess [number]`). + - [ ] Create a leaderboard for game winners. + +--- + +#### **Utility Tools** +1. **Search and Lookups** + - [ ] Add a `!wiki [topic]` command to fetch summaries from Wikipedia. + - [ ] Include a `!weather [location]` command for weather updates. + +--- + +#### **Advanced and Fun Features (very optional)** +1. **AI Chat Integration** + - [ ] Integrate GPT for conversational AI: + - [ ] Respond to mentions with intelligent replies. + - [ ] Allow users to ask general questions with `!ask [question]`. + +--- + +#### **Deployment (will happen if im completly satisfied with the bot)** +1. **Hosting** + - [ ] Set up local hosting for development. + - [ ] Deploy the bot on a cloud platform (e.g., Heroku, AWS, or Railway). + +2. **Monitoring** + - [ ] Add logging to track errors and usage statistics. + - [ ] Implement uptime monitoring to ensure the bot stays online. From f7ac4773ac1a55792b24212b229a53e87bd9cb40 Mon Sep 17 00:00:00 2001 From: ScriptSorcerer20 Date: Sat, 25 Jan 2025 16:56:46 +0100 Subject: [PATCH 02/10] Implemented new Commands: help - list all commands timestamp - convert date to timestamp (still in progress) about - Bot porpose (still in progress) ping - check bot availability --- README.md | 21 ++++++------- commands/unique/timestamp.js | 60 ++++++++++++++++++++++++++++++++++++ commands/utility/about.js | 13 ++++++++ commands/utility/help.js | 23 ++++++++++++++ commands/utility/ping.js | 4 +-- package-lock.json | 10 ++++++ package.json | 1 + 7 files changed, 118 insertions(+), 14 deletions(-) create mode 100644 commands/unique/timestamp.js create mode 100644 commands/utility/about.js create mode 100644 commands/utility/help.js diff --git a/README.md b/README.md index cbb2413..4538c8b 100644 --- a/README.md +++ b/README.md @@ -6,19 +6,15 @@ Im going to make a To-do List for the features i want to implement for my Bot. (I will try to make these tools optional, because alot of bots already feature them) 1. **Basic Commands** - - [ ] Add a `!help` command to display a list of available commands. - - [ ] Implement a `!ping` command to check bot responsiveness. - - [ ] Add a `!about` command to describe the bot's purpose. + - [x] Add a `!help` command to display a list of available commands. + - [x] Implement a `!ping` command to check bot responsiveness. + - [ **in progress** ] Add a `!about` command to describe the bot's purpose. -2. **Welcome and Goodbye System** - - [ ] Create a system to welcome new users to the server with a customizable message. - - [ ] Implement a goodbye message when users leave the server. - -3. **Role Management** +2. **Role Management** - [ ] Set up a self-assignable role system (e.g., using reaction roles). - [ ] Allow admins to assign/remove roles with commands. -4. **Moderation Tools** +3. **Moderation Tools** - [ ] Add commands to mute, kick, and ban users. - [ ] Create an auto-moderation feature to delete spam or inappropriate content. - [ ] Implement a warning system with a strike counter. @@ -35,9 +31,10 @@ Im going to make a To-do List for the features i want to implement for my Bot. - [ ] Allow users to purchase roles using the currency with a command (e.g., `!buyrole [role name]`). 2. **Datetime Converter** - - [ ] Create a command to convert timezones: - - [ ] `!convert [time] [timezone] to [timezone]` to handle conversions. - - [ ] Support both 12-hour and 24-hour formats. + (Still in Progress, maybe i'll add visual Menu) + - [x] Create a command to convert timezones: + - [x] `!convert [time] [timezone] to [timezone]` to handle conversions. + - [x] Support both 12-hour and 24-hour formats. - [ ] Include a command to display server-specific or user-specific timezones. 3. **Minigames** diff --git a/commands/unique/timestamp.js b/commands/unique/timestamp.js new file mode 100644 index 0000000..01f2b57 --- /dev/null +++ b/commands/unique/timestamp.js @@ -0,0 +1,60 @@ +const { SlashCommandBuilder } = require('discord.js'); +const moment = require('moment'); // Install moment.js with `npm install moment` + +module.exports = { + data: new SlashCommandBuilder() + .setName('timestamp') + .setDescription('Convert a human-readable date to a Discord timestamp.') + .addStringOption(option => + option + .setName('date') + .setDescription('The date to convert (e.g., YYYY-MM-DD HH:mm)') + .setRequired(true) + ) + .addStringOption(option => + option + .setName('format') + .setDescription('The format for the timestamp') + .setRequired(false) + .addChoices( + { name: 'Short time', value: 't' }, + { name: 'Long time', value: 'T' }, + { name: 'Short date', value: 'd' }, + { name: 'Long date', value: 'D' }, + { name: 'Full date and time', value: 'f' }, + { name: 'Full date and time with weekday', value: 'F' }, + { name: 'Relative time', value: 'R' }, + )), + async execute(interaction) { + // Get user input + const dateInput = interaction.options.getString('date'); + const format = interaction.options.getString('format') || 'f'; // Default to 'f' format + + try { + // Convert the date to a Unix timestamp + const unixTimestamp = moment(dateInput, 'YYYY-MM-DD HH:mm').unix(); + + if (isNaN(unixTimestamp)) { + return interaction.reply({ + content: 'Invalid date. Use the format `YYYY-MM-DD HH:mm`.', + ephemeral: true, + }); + } + + // Construct the Discord timestamp + const discordTimestamp = ``; + + // Reply with the result + await interaction.reply( + `Here’s your Discord timestamp:\n${discordTimestamp}` + ); + } catch (error) { + console.error(error); + await interaction.reply({ + content: + 'Invalid date format. Please use the format `YYYY-MM-DD HH:mm` (e.g., `2025-01-25 14:30`).', + ephemeral: true, + }); + } + }, +}; \ No newline at end of file diff --git a/commands/utility/about.js b/commands/utility/about.js new file mode 100644 index 0000000..7bcf4c1 --- /dev/null +++ b/commands/utility/about.js @@ -0,0 +1,13 @@ +const {SlashCommandBuilder} = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("about") + .setDescription("Information about this bot"), + async execute(interaction) { + await interaction.reply({ + content: "This bot has some general commands for moderation, but includes on or two unqiue features, like the currenc system. It's a small project to enhanced the programming skills of the creator.", + ephemeral: true + }) + } +} \ No newline at end of file diff --git a/commands/utility/help.js b/commands/utility/help.js new file mode 100644 index 0000000..9936a41 --- /dev/null +++ b/commands/utility/help.js @@ -0,0 +1,23 @@ +const { SlashCommandBuilder} = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName('help') + .setDescription('Gives all available commands.'), + async execute(interaction) { + // Map over the full command objects to get names and descriptions + const commandDetails = interaction.client.commands.map(cmd => ({ + name: cmd.data.name, + description: cmd.data.description, + })); + + // Format the commands into a user-friendly help message + const helpMessage = commandDetails + .map(detail => `/${detail.name} - ${detail.description}`) + .join('\n'); + + // Send the help message + await interaction.reply(`Here are all the available commands:\n\n${helpMessage}`); + + }, +}; \ No newline at end of file diff --git a/commands/utility/ping.js b/commands/utility/ping.js index 2fc8932..2b6a058 100644 --- a/commands/utility/ping.js +++ b/commands/utility/ping.js @@ -3,8 +3,8 @@ const { SlashCommandBuilder } = require('discord.js'); module.exports = { data: new SlashCommandBuilder() .setName('ping') - .setDescription('Replies with Pong!'), + .setDescription('Check if the bots responsivnes'), async execute(interaction) { - await interaction.reply('Pong!'); + await interaction.reply({ content: 'Pong! (Online)', ephemeral: true }); }, }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ed1719c..bc330ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "better-sqlite3": "^11.3.0", "discord.js": "^14.17.3", "express": "^4.21.0", + "moment": "^2.30.1", "openai": "^4.67.3", "sqlite3": "^5.1.7" } @@ -1738,6 +1739,15 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index a0a5fec..04bda05 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "better-sqlite3": "^11.3.0", "discord.js": "^14.17.3", "express": "^4.21.0", + "moment": "^2.30.1", "openai": "^4.67.3", "sqlite3": "^5.1.7" } From e89cc4641380ba390a61a7705232951f809a475b Mon Sep 17 00:00:00 2001 From: ScriptSorcerer20 Date: Tue, 28 Jan 2025 22:40:05 +0100 Subject: [PATCH 03/10] added kick command --- commands/moderation/kick.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 commands/moderation/kick.js diff --git a/commands/moderation/kick.js b/commands/moderation/kick.js new file mode 100644 index 0000000..00d19e8 --- /dev/null +++ b/commands/moderation/kick.js @@ -0,0 +1,34 @@ +const { SlashCommandBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("kick") + .setDescription("Kick misbehaving people") + .addUserOption(option => + option + .setName('target') + .setDescription('The member to kick') + .setRequired(true)) + .addStringOption(option => + option + .setName('reason') + .setDescription('The reason of the kick')), + async execute(interaction) { + const user = interaction.options.getUser('target'); + const reason = interaction.options.getString('reason') || 'No reason provided'; + + try { + const member = await interaction.guild.members.fetch(user.id); + + await member.kick(reason); + + await interaction.reply(`Kicked ${user.tag} for: ${reason}`); + } catch (error) { + console.error(error); + await interaction.reply({ + content: 'I was not able to kick the user..', + ephemeral: true, + }); + } + }, +}; From ea0507dd16601778313d35ca6de6a60661f172ee Mon Sep 17 00:00:00 2001 From: ScriptSorcerer20 Date: Thu, 30 Jan 2025 20:15:50 +0100 Subject: [PATCH 04/10] added mognodb database --- README.md | 4 +- commands/db/test.js | 27 ++++++ commands/moderation/mute.js | 44 ++++++++++ commands/moderation/temp_ban.js | 0 commands/unique/quiz.js | 45 ++++++++++ commands/unique/quiz.json | 10 +++ commands/utility/about.js | 14 ++- db.js | 30 +++++++ events/ready.js | 10 ++- package-lock.json | 148 ++++++++++++++++++++++++++++++++ package.json | 1 + 11 files changed, 326 insertions(+), 7 deletions(-) create mode 100644 commands/db/test.js create mode 100644 commands/moderation/mute.js create mode 100644 commands/moderation/temp_ban.js create mode 100644 commands/unique/quiz.js create mode 100644 commands/unique/quiz.json create mode 100644 db.js diff --git a/README.md b/README.md index 4538c8b..c5c10a4 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ Im going to make a To-do List for the features i want to implement for my Bot. 1. **Basic Commands** - [x] Add a `!help` command to display a list of available commands. - [x] Implement a `!ping` command to check bot responsiveness. - - [ **in progress** ] Add a `!about` command to describe the bot's purpose. + - [ **Only Text missing** ] Add a `!about` command to describe the bot's purpose. 2. **Role Management** - [ ] Set up a self-assignable role system (e.g., using reaction roles). - [ ] Allow admins to assign/remove roles with commands. 3. **Moderation Tools** - - [ ] Add commands to mute, kick, and ban users. + - [ ] Add commands to mute, x kick, and ban users. - [ ] Create an auto-moderation feature to delete spam or inappropriate content. - [ ] Implement a warning system with a strike counter. diff --git a/commands/db/test.js b/commands/db/test.js new file mode 100644 index 0000000..3adc425 --- /dev/null +++ b/commands/db/test.js @@ -0,0 +1,27 @@ +// exampleCommand.js +const { SlashCommandBuilder } = require('discord.js'); +const { connectToDatabase } = require('../../db.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('example') + .setDescription('An example DB usage') + .addStringOption(option => + option + .setName('message') + .setDescription('string to store') + .setRequired(true)), + async execute(interaction) { + const dbClient = await connectToDatabase(); + const message = interaction.options.getString('message'); + + // Choose a DB + Collection + const db = dbClient.db('discord'); + const collection = db.collection('test'); + + // Example: Insert something + await collection.insertOne({ userId: interaction.user.id, message: message }); + + await interaction.reply('Successfully inserted your data into MongoDB.'); + }, +}; diff --git a/commands/moderation/mute.js b/commands/moderation/mute.js new file mode 100644 index 0000000..5d4df3b --- /dev/null +++ b/commands/moderation/mute.js @@ -0,0 +1,44 @@ +const { SlashCommandBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("mute") + .setDescription("mute misbehaving people") + .addUserOption(option => + option + .setName('target') + .setDescription('The member to mute') + .setRequired(true)) + .addIntegerOption(option => + option + .setName('duration') + .setDescription('The duration of the Mute (Minutes)') + .setRequired(true)) + .addStringOption(option => + option + .setName('reason') + .setDescription('The reason of the mute')), + async execute(interaction) { + // Get the user and duration from the command options + const target = interaction.options.getUser('target'); + const duration = interaction.options.getInteger('duration'); + + // Convert duration to milliseconds + const durationMs = duration * 60 * 1000; + + try { + // Apply the timeout + await target.timeout(durationMs); + + await interaction.reply({ + content: `${target.tag} has been muted for ${duration} minutes.` + }); + } catch (error) { + console.error(error); + await interaction.reply({ + content: 'I was unable to mute the member.', + ephemeral: true, + }); + } + }, +}; \ No newline at end of file diff --git a/commands/moderation/temp_ban.js b/commands/moderation/temp_ban.js new file mode 100644 index 0000000..e69de29 diff --git a/commands/unique/quiz.js b/commands/unique/quiz.js new file mode 100644 index 0000000..6956d7f --- /dev/null +++ b/commands/unique/quiz.js @@ -0,0 +1,45 @@ +const quiz = require('./quiz.json'); +const {SlashCommandBuilder} = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("quiz") + .setDescription("Start a cool quiz"), + async execute(interaction) { + const item = quiz[Math.floor(Math.random() * quiz.length)]; + await interaction.reply({ + content: `**Quiz Time!**\n${item.question}\n\n_You have 30 seconds to answer!_`, + }); + + // Define a filter to check the correct answer + const collectorFilter = (response) => { + return ( + response.author.id === interaction.user.id && // Ensure it's the same user + item.answers.some( + (answer) => answer.toLowerCase() === response.content.toLowerCase() + ) + ); + }; + + try { + // Await messages in the same channel + const collected = await interaction.channel.awaitMessages({ + filter: collectorFilter, + max: 1, + time: 30000, // 30 seconds + errors: ['time'], + }); + + // If a message is collected, reply with the correct response + const correctAnswer = collected.first(); + await interaction.followUp( + `${correctAnswer.author} got the correct answer: **${correctAnswer.content}**! 🎉` + ); + } catch (err) { + // If no one answers in time + await interaction.followUp( + `⏳ Time's up! Nobody got the correct answer. The correct answers were: **${item.answers.join(', ')}**.` + ); + } + }, +}; \ No newline at end of file diff --git a/commands/unique/quiz.json b/commands/unique/quiz.json new file mode 100644 index 0000000..260f757 --- /dev/null +++ b/commands/unique/quiz.json @@ -0,0 +1,10 @@ +[ + { + "question": "What color is the sky?", + "answers": ["blue"] + }, + { + "question": "How many letters are there in the alphabet?", + "answers": ["26", "twenty-six", "twenty six", "twentysix"] + } +] diff --git a/commands/utility/about.js b/commands/utility/about.js index 7bcf4c1..c809cd3 100644 --- a/commands/utility/about.js +++ b/commands/utility/about.js @@ -1,13 +1,19 @@ -const {SlashCommandBuilder} = require("discord.js"); +const {SlashCommandBuilder, EmbedBuilder} = require("discord.js"); module.exports = { data: new SlashCommandBuilder() .setName("about") - .setDescription("Information about this bot"), + .setDescription("Information about this bot and creator"), async execute(interaction) { + const exampleEmbed = new EmbedBuilder() + .setColor(0x0099FF) + .setTitle('About Page') + .setDescription('I build this bot for managing my servers. (to lazy to write mor text rn)') + .setImage('https://i.postimg.cc/QdZZwKVD/Screenshot-2024-12-25-005933.png') + .setTimestamp(); + await interaction.reply({ - content: "This bot has some general commands for moderation, but includes on or two unqiue features, like the currenc system. It's a small project to enhanced the programming skills of the creator.", - ephemeral: true + embeds: [exampleEmbed] }) } } \ No newline at end of file diff --git a/db.js b/db.js new file mode 100644 index 0000000..d2a0598 --- /dev/null +++ b/db.js @@ -0,0 +1,30 @@ +const { MongoClient, ServerApiVersion } = require('mongodb'); +const { mongodb } = require("./config.json"); + +let dbClient; + +async function connectToDatabase() { + if (!dbClient) { + // Create a new MongoClient only once + dbClient = new MongoClient(mongodb, { + serverApi: { + version: ServerApiVersion.v1, + strict: true, + deprecationErrors: true, + } + }); + + await dbClient.connect(); + console.log("Connected to MongoDB!"); + + await dbClient.db("admin").command({ ping: 1 }); + console.log("Pinged your deployment. You successfully connected to MongoDB!"); + } + + // Return the existing connection if already connected + return dbClient; +} + +module.exports = { + connectToDatabase +}; diff --git a/events/ready.js b/events/ready.js index ea615aa..7115c92 100644 --- a/events/ready.js +++ b/events/ready.js @@ -1,9 +1,17 @@ const { Events } = require('discord.js'); +const { connectToDatabase } = require('../db.js'); module.exports = { name: Events.ClientReady, once: true, - execute(client) { + async execute(client) { console.log(`Ready! Logged in as ${client.user.tag}`); + + try { + await connectToDatabase(); + + } catch (err) { + console.error("MongoDB connection error:", err); + } }, }; diff --git a/package-lock.json b/package-lock.json index bc330ff..76fde88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "discord.js": "^14.17.3", "express": "^4.21.0", "moment": "^2.30.1", + "mongodb": "^6.12.0", "openai": "^4.67.3", "sqlite3": "^5.1.7" } @@ -152,6 +153,15 @@ "license": "MIT", "optional": true }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", + "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, "node_modules/@npmcli/fs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", @@ -240,6 +250,21 @@ "form-data": "^4.0.0" } }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, "node_modules/@types/ws": { "version": "8.5.14", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz", @@ -505,6 +530,15 @@ "concat-map": "0.0.1" } }, + "node_modules/bson": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.2.tgz", + "integrity": "sha512-5afhLTjqDSA3akH56E+/2J6kTDuSIlBxyXPdQslj9hcIgOUE378xdOfZvC/9q3LifJNI6KR/juZ+d0NRNYBwXg==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -1541,6 +1575,12 @@ "node": ">= 0.6" } }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -1748,6 +1788,96 @@ "node": "*" } }, + "node_modules/mongodb": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.12.0.tgz", + "integrity": "sha512-RM7AHlvYfS7jv7+BXund/kR64DryVI+cHbVAy9P61fnb1RcWZqOW1/Wj2YhqMCx+MuYhqTRGv7AwHBzmsCKBfA==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.9", + "bson": "^6.10.1", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/whatwg-url": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz", + "integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==", + "license": "MIT", + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2075,6 +2205,15 @@ "once": "^1.3.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -2424,6 +2563,15 @@ "node": ">= 10" } }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", diff --git a/package.json b/package.json index 04bda05..afd5dba 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "discord.js": "^14.17.3", "express": "^4.21.0", "moment": "^2.30.1", + "mongodb": "^6.12.0", "openai": "^4.67.3", "sqlite3": "^5.1.7" } From 0d265b0fb59c89cb95f50f30ffaf75adf8a04875 Mon Sep 17 00:00:00 2001 From: ScriptSorcerer20 Date: Wed, 12 Mar 2025 19:35:49 +0100 Subject: [PATCH 05/10] startet currency system, think i added temporary ban and a give command --- README.md | 2 +- commands/db/balance.js | 0 commands/db/give.js | 37 ++++++++++++ commands/moderation/mute.js | 20 +++--- commands/moderation/temp_ban.js | 104 ++++++++++++++++++++++++++++++++ deploy-commands.js | 2 +- events/tempban.js | 44 ++++++++++++++ 7 files changed, 194 insertions(+), 15 deletions(-) create mode 100644 commands/db/balance.js create mode 100644 commands/db/give.js create mode 100644 events/tempban.js diff --git a/README.md b/README.md index c5c10a4..753474b 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Im going to make a To-do List for the features i want to implement for my Bot. - [ ] Allow admins to assign/remove roles with commands. 3. **Moderation Tools** - - [ ] Add commands to mute, x kick, and ban users. + - [ ] Add commands to mute, x kick, and x temp_ban users. - [ ] Create an auto-moderation feature to delete spam or inappropriate content. - [ ] Implement a warning system with a strike counter. diff --git a/commands/db/balance.js b/commands/db/balance.js new file mode 100644 index 0000000..e69de29 diff --git a/commands/db/give.js b/commands/db/give.js new file mode 100644 index 0000000..4bc2441 --- /dev/null +++ b/commands/db/give.js @@ -0,0 +1,37 @@ +// exampleCommand.js +const { SlashCommandBuilder } = require('discord.js'); +const { connectToDatabase } = require('../../db.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('give') + .setDescription('give somebody coins') + .addIntegerOption(option => + option + .setName('amount') + .setDescription('amount of the coins') + .setRequired(true)) + .addUserOption(option => + option.setName('target') + .setDescription('The user to give coins to') + .setRequired(true)), + async execute(interaction) { + const dbClient = await connectToDatabase(); + const target = interaction.options.getUser('target'); + const amount = interaction.options.getInteger('amount'); + + // Choose a DB + Collection + const db = dbClient.db('discord'); + const collection = db.collection('currency'); + + let targetData = await collection.findOne({ userID: target.id }); + if (!targetData) { + await collection.insertOne({ userID: target.id, balance: 0 }); + } + + // Perform the transaction: subtract from sender and add to target. + await collection.updateOne({ userID: target.id }, { $inc: { balance: amount } }, { upsert: true }); + + return interaction.reply({ content: `You have given ${amount} coins to ${target.tag}.` }); + } +}; \ No newline at end of file diff --git a/commands/moderation/mute.js b/commands/moderation/mute.js index 5d4df3b..09d56a8 100644 --- a/commands/moderation/mute.js +++ b/commands/moderation/mute.js @@ -19,26 +19,20 @@ module.exports = { .setName('reason') .setDescription('The reason of the mute')), async execute(interaction) { - // Get the user and duration from the command options const target = interaction.options.getUser('target'); const duration = interaction.options.getInteger('duration'); + const reason = interaction.options.getString('reason'); // Convert duration to milliseconds const durationMs = duration * 60 * 1000; - try { - // Apply the timeout - await target.timeout(durationMs); + const guildmember = await interaction.guild.members.fetch(target.id); - await interaction.reply({ - content: `${target.tag} has been muted for ${duration} minutes.` - }); - } catch (error) { - console.error(error); - await interaction.reply({ - content: 'I was unable to mute the member.', - ephemeral: true, - }); + const muteRole = interaction.guild.roles.find(role => role.name === "mute"); + if (!muteRole) { + + } else { + await guildmember.roles.add(muteRole, reason); } }, }; \ No newline at end of file diff --git a/commands/moderation/temp_ban.js b/commands/moderation/temp_ban.js index e69de29..871e467 100644 --- a/commands/moderation/temp_ban.js +++ b/commands/moderation/temp_ban.js @@ -0,0 +1,104 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); +const { connectToDatabase } = require('../../db.js'); + +module.exports = { + mod: true, + data: new SlashCommandBuilder() + .setName("temp-ban") + .setDescription("Kick misbehaving people") + .addUserOption(option => + option + .setName('target') + .setDescription('The member to kick') + .setRequired(true)) + .addIntegerOption(option => + option + .setName('durationms') + .setDescription('Duration of the temporary ban (in milliseconds).') + .setRequired(true)) + .addStringOption(option => + option + .setName('reason') + .setDescription('Reason for the ban') + .setRequired(true)) + .addIntegerOption(option => + option + .setName('delete-messages') + .setDescription('Number of days to delete messages') + .setMinValue(0).setMaxValue(7)), + async execute(interaction) { + const user = interaction.options.getUser('target'); + const reason = interaction.options.getString('reason'); + const durationms = interaction.options.getInteger('durationms'); + const deleteMessages = interaction.options.getInteger('delete-messages'); + + // If you want to parse time like "1d", "10m", etc., use ms package: + let banUntil; + try { + banUntil = Date.now() + durationms; + } catch (e) { + // If parsing fails, handle error + return interaction.reply({ + content: 'Invalid duration format. Please use something like "10m" or "1d".', + ephemeral: true + }); + } + + + // A function to send ephemeral messages + async function sendEphemeral(message) { + const embed = new EmbedBuilder() + .setColor("Blurple") + .setDescription(message); + + return interaction.reply({ embeds: [embed], ephemeral: true }); + } + + // Check if user is already banned + const bans = await interaction.guild.bans.fetch(); + if (bans.has(user.id)) { + return sendEphemeral("That user is already banned"); + } + + // --------- STORE BAN DATA IN MONGODB --------- + try { + // 1. Get or create the MongoDB client + const dbClient = await connectToDatabase(); + + // 2. Access a DB and a collection + const db = dbClient.db("discord"); + const tempBanCollection = db.collection("tempban"); + + // 3. Insert the ban record + await tempBanCollection.insertOne({ + guildId: interaction.guild.id, + userId: user.id, + banTime: banUntil, + reason: reason, + }); + } catch (err) { + console.error(err); + return sendEphemeral("Could not insert ban record into DB."); + } + + // --------- BAN THE USER IN DISCORD --------- + let error = false; + try { + await interaction.guild.bans.create(user.id, { + reason: reason, + deleteMessageDays: deleteMessages, + }); + } catch (err) { + error = true; + console.error(err); + } + + if (error) { + return sendEphemeral("Something went wrong banning the user."); + } else { + return sendEphemeral( + `${user} has been banned for **${durationms}** for *${reason}*` + ); + } + }, +}; diff --git a/deploy-commands.js b/deploy-commands.js index ed61030..fd3b346 100644 --- a/deploy-commands.js +++ b/deploy-commands.js @@ -34,7 +34,7 @@ const rest = new REST().setToken(token); // The put method is used to fully refresh all commands in the guild with the current set const data = await rest.put( - Routes.applicationGuildCommands(clientId, guildId), + Routes.applicationCommands(clientId), { body: commands }, ); diff --git a/events/tempban.js b/events/tempban.js new file mode 100644 index 0000000..dd2e9ab --- /dev/null +++ b/events/tempban.js @@ -0,0 +1,44 @@ +const { Events } = require('discord.js'); +const { connectToDatabase } = require('../db.js'); + +module.exports = { + name: Events.ClientReady, + async execute(client) { + const dbClient = await connectToDatabase(); + const db = dbClient.db('discord'); + const tempBanCollection = db.collection('tempban'); + + // 2) Define a function to schedule unbans + async function scheduleUnban(banData) { + const delay = banData.banTime - Date.now(); + + if (delay <= 0) { + return; + } + setTimeout(async () => { + try { + const guild = await client.guilds.fetch(banData.guildId); + await guild.bans.remove(banData.userId); + + await tempBanCollection.deleteOne({ _id: banData._id }); + } catch (err) { + console.error('Error unbanning member:', err); + } + }, delay); + } + + try { + const allBans = await tempBanCollection.find({}).toArray(); + allBans.forEach(scheduleUnban); + + tempBanCollection.watch().on('change', (change) => { + if (change.operationType === 'insert') { + const newBan = change.fullDocument; + scheduleUnban(newBan); + } + }); + } catch (err) { + console.error('Error setting up unban system:', err); + } + }, +}; From 7662337553c87f4ae668bdc5b97e1e8d413d4a48 Mon Sep 17 00:00:00 2001 From: Fretux Date: Mon, 26 Jan 2026 11:34:48 +0100 Subject: [PATCH 06/10] Refactor command and event loading --- deploy-commands.js | 53 +++++++++++++++++---------------------------- index.js | 39 ++++++++++++++------------------- loaders/commands.js | 41 +++++++++++++++++++++++++++++++++++ loaders/events.js | 23 ++++++++++++++++++++ 4 files changed, 100 insertions(+), 56 deletions(-) create mode 100644 loaders/commands.js create mode 100644 loaders/events.js diff --git a/deploy-commands.js b/deploy-commands.js index fd3b346..de5dd15 100644 --- a/deploy-commands.js +++ b/deploy-commands.js @@ -1,27 +1,14 @@ -const { REST, Routes } = require('discord.js'); -const { clientId, guildId, token } = require('./config.json'); -const fs = require('node:fs'); const path = require('node:path'); +const { REST, Routes } = require('discord.js'); +const { clientId, token } = require('./config.json'); +const { loadCommandModules } = require('./loaders/commands'); -const commands = []; -// Grab all the command folders from the commands directory you created earlier -const foldersPath = path.join(__dirname, 'commands'); -const commandFolders = fs.readdirSync(foldersPath); +const commandsPath = path.join(__dirname, 'commands'); +const { commands, invalidCommands } = loadCommandModules(commandsPath); +const commandsData = commands.map((command) => command.data.toJSON()); -for (const folder of commandFolders) { - // Grab all the command files from the commands directory you created earlier - const commandsPath = path.join(foldersPath, folder); - const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js')); - // Grab the SlashCommandBuilder#toJSON() output of each command's data for deployment - 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.`); - } - } +for (const filePath of invalidCommands) { + console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`); } // Construct and prepare an instance of the REST module @@ -29,18 +16,18 @@ const rest = new REST().setToken(token); // and deploy your commands! (async () => { - try { - console.log(`Started refreshing ${commands.length} application (/) commands.`); + try { + console.log(`Started refreshing ${commandsData.length} application (/) commands.`); - // The put method is used to fully refresh all commands in the guild with the current set - const data = await rest.put( - Routes.applicationCommands(clientId), - { body: commands }, - ); + // The put method is used to fully refresh all commands in the guild with the current set + const data = await rest.put( + Routes.applicationCommands(clientId), + { body: commandsData }, + ); - console.log(`Successfully reloaded ${data.length} application (/) commands.`); - } catch (error) { - // And of course, make sure you catch and log any errors! - console.error(error); - } + console.log(`Successfully reloaded ${data.length} application (/) commands.`); + } catch (error) { + // And of course, make sure you catch and log any errors! + console.error(error); + } })(); diff --git a/index.js b/index.js index 4552850..87f3086 100644 --- a/index.js +++ b/index.js @@ -1,36 +1,29 @@ -const fs = require('node:fs'); const path = require('node:path'); const { Client, Collection, GatewayIntentBits } = require('discord.js'); const { token } = require('./config.json'); +const { loadCommandModules } = require('./loaders/commands'); +const { loadEventModules } = require('./loaders/events'); const client = new Client({ intents: [GatewayIntentBits.Guilds] }); -// Retreaves commands from the command folder +// Retrieves commands from the command folder client.commands = new Collection(); -const foldersPath = path.join(__dirname, 'commands'); -const commandFolders = fs.readdirSync(foldersPath); +const commandsPath = path.join(__dirname, 'commands'); +const { commands, invalidCommands } = loadCommandModules(commandsPath); -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.`); - } - } +for (const command of commands) { + client.commands.set(command.data.name, command); } -// Retreaves Events from the events folder -const eventsPath = path.join(__dirname, 'events'); -const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js')); +for (const filePath of invalidCommands) { + console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`); +} -for (const file of eventFiles) { - const filePath = path.join(eventsPath, file); - const event = require(filePath); +// Retrieves events from the events folder +const eventsPath = path.join(__dirname, 'events'); +const eventModules = loadEventModules(eventsPath); + +for (const { event } of eventModules) { if (event.once) { client.once(event.name, (...args) => event.execute(...args)); } else { @@ -38,4 +31,4 @@ for (const file of eventFiles) { } } -client.login(token); \ No newline at end of file +client.login(token); diff --git a/loaders/commands.js b/loaders/commands.js new file mode 100644 index 0000000..94f0d35 --- /dev/null +++ b/loaders/commands.js @@ -0,0 +1,41 @@ +const fs = require('node:fs'); +const path = require('node:path'); + +const isJavaScriptFile = (file) => file.endsWith('.js'); + +const getCommandFilePaths = (commandsPath) => { + const commandFolders = fs + .readdirSync(commandsPath, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name); + + return commandFolders.flatMap((folder) => { + const folderPath = path.join(commandsPath, folder); + return fs + .readdirSync(folderPath) + .filter(isJavaScriptFile) + .map((file) => path.join(folderPath, file)); + }); +}; + +const loadCommandModules = (commandsPath) => { + const commandFiles = getCommandFilePaths(commandsPath); + const commands = []; + const invalidCommands = []; + + for (const filePath of commandFiles) { + const command = require(filePath); + + if ('data' in command && 'execute' in command) { + commands.push(command); + } else { + invalidCommands.push(filePath); + } + } + + return { commands, invalidCommands }; +}; + +module.exports = { + loadCommandModules, +}; diff --git a/loaders/events.js b/loaders/events.js new file mode 100644 index 0000000..e1668fc --- /dev/null +++ b/loaders/events.js @@ -0,0 +1,23 @@ +const fs = require('node:fs'); +const path = require('node:path'); + +const isJavaScriptFile = (file) => file.endsWith('.js'); + +const getEventFilePaths = (eventsPath) => + fs + .readdirSync(eventsPath) + .filter(isJavaScriptFile) + .map((file) => path.join(eventsPath, file)); + +const loadEventModules = (eventsPath) => { + const eventFiles = getEventFilePaths(eventsPath); + + return eventFiles.map((filePath) => ({ + filePath, + event: require(filePath), + })); +}; + +module.exports = { + loadEventModules, +}; From 022e09a1f363697fe2a19863ceb394eb448da9e7 Mon Sep 17 00:00:00 2001 From: Fretux Date: Mon, 26 Jan 2026 12:14:18 +0100 Subject: [PATCH 07/10] Add currency commands and polish utilities --- commands/db/balance.js | 26 +++++++++++++++ commands/db/earn.js | 59 +++++++++++++++++++++++++++++++++ commands/db/give.js | 40 ++++++++++++++--------- commands/db/transfer.js | 68 +++++++++++++++++++++++++++++++++++++++ commands/utility/about.js | 18 +++++++---- commands/utility/help.js | 26 ++++++++------- commands/utility/ping.js | 13 ++++++-- 7 files changed, 213 insertions(+), 37 deletions(-) create mode 100644 commands/db/earn.js create mode 100644 commands/db/transfer.js diff --git a/commands/db/balance.js b/commands/db/balance.js index e69de29..32b274e 100644 --- a/commands/db/balance.js +++ b/commands/db/balance.js @@ -0,0 +1,26 @@ +const { SlashCommandBuilder } = require('discord.js'); +const { connectToDatabase } = require('../../db.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('balance') + .setDescription('Check your coin balance or another user\'s balance.') + .addUserOption(option => + option + .setName('user') + .setDescription('User to check (defaults to you)') + .setRequired(false)), + async execute(interaction) { + const dbClient = await connectToDatabase(); + const db = dbClient.db('discord'); + const collection = db.collection('currency'); + + const target = interaction.options.getUser('user') ?? interaction.user; + const targetData = await collection.findOne({ userID: target.id }); + const balance = targetData?.balance ?? 0; + + return interaction.reply({ + content: `${target.tag} has **${balance}** coins.`, + }); + } +}; diff --git a/commands/db/earn.js b/commands/db/earn.js new file mode 100644 index 0000000..6853b1e --- /dev/null +++ b/commands/db/earn.js @@ -0,0 +1,59 @@ +const { SlashCommandBuilder } = require('discord.js'); +const { connectToDatabase } = require('../../db.js'); + +const REWARD_AMOUNT = 100; +const COOLDOWN_MS = 24 * 60 * 60 * 1000; + +const formatDuration = (ms) => { + const totalSeconds = Math.ceil(ms / 1000); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + const parts = []; + if (hours) parts.push(`${hours}h`); + if (minutes) parts.push(`${minutes}m`); + if (!hours && !minutes) parts.push(`${seconds}s`); + + return parts.join(' '); +}; + +module.exports = { + data: new SlashCommandBuilder() + .setName('earn') + .setDescription('Claim your daily coin reward.'), + async execute(interaction) { + const dbClient = await connectToDatabase(); + const db = dbClient.db('discord'); + const collection = db.collection('currency'); + const now = new Date(); + + const userId = interaction.user.id; + const userData = await collection.findOne({ userID: userId }); + const lastClaim = userData?.lastClaimedAt ? new Date(userData.lastClaimedAt) : null; + + if (lastClaim) { + const elapsed = now - lastClaim; + if (elapsed < COOLDOWN_MS) { + const remaining = COOLDOWN_MS - elapsed; + return interaction.reply({ + content: `You already claimed your reward. Try again in **${formatDuration(remaining)}**.`, + ephemeral: true, + }); + } + } + + await collection.updateOne( + { userID: userId }, + { + $set: { lastClaimedAt: now.toISOString() }, + $inc: { balance: REWARD_AMOUNT }, + }, + { upsert: true } + ); + + return interaction.reply({ + content: `You earned **${REWARD_AMOUNT}** coins! Come back tomorrow for more.`, + }); + } +}; diff --git a/commands/db/give.js b/commands/db/give.js index 4bc2441..77a6548 100644 --- a/commands/db/give.js +++ b/commands/db/give.js @@ -1,37 +1,45 @@ -// exampleCommand.js -const { SlashCommandBuilder } = require('discord.js'); +const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js'); const { connectToDatabase } = require('../../db.js'); module.exports = { data: new SlashCommandBuilder() .setName('give') - .setDescription('give somebody coins') + .setDescription('Admin: grant coins to a user.') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) .addIntegerOption(option => option .setName('amount') - .setDescription('amount of the coins') + .setDescription('Amount of coins to give') + .setMinValue(1) .setRequired(true)) .addUserOption(option => - option.setName('target') - .setDescription('The user to give coins to') + option + .setName('target') + .setDescription('User to receive coins') .setRequired(true)), async execute(interaction) { const dbClient = await connectToDatabase(); const target = interaction.options.getUser('target'); const amount = interaction.options.getInteger('amount'); - // Choose a DB + Collection + if (target.bot) { + return interaction.reply({ + content: 'You cannot grant coins to bot accounts.', + ephemeral: true, + }); + } + const db = dbClient.db('discord'); const collection = db.collection('currency'); - let targetData = await collection.findOne({ userID: target.id }); - if (!targetData) { - await collection.insertOne({ userID: target.id, balance: 0 }); - } + await collection.updateOne( + { userID: target.id }, + { $inc: { balance: amount } }, + { upsert: true } + ); - // Perform the transaction: subtract from sender and add to target. - await collection.updateOne({ userID: target.id }, { $inc: { balance: amount } }, { upsert: true }); - - return interaction.reply({ content: `You have given ${amount} coins to ${target.tag}.` }); + return interaction.reply({ + content: `You granted **${amount}** coins to ${target.tag}.`, + }); } -}; \ No newline at end of file +}; diff --git a/commands/db/transfer.js b/commands/db/transfer.js new file mode 100644 index 0000000..b2adda2 --- /dev/null +++ b/commands/db/transfer.js @@ -0,0 +1,68 @@ +const { SlashCommandBuilder } = require('discord.js'); +const { connectToDatabase } = require('../../db.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('transfer') + .setDescription('Send coins to another user.') + .addUserOption(option => + option + .setName('target') + .setDescription('User to send coins to') + .setRequired(true)) + .addIntegerOption(option => + option + .setName('amount') + .setDescription('Amount of coins to send') + .setMinValue(1) + .setRequired(true)), + async execute(interaction) { + const dbClient = await connectToDatabase(); + const db = dbClient.db('discord'); + const collection = db.collection('currency'); + + const target = interaction.options.getUser('target'); + const amount = interaction.options.getInteger('amount'); + + if (target.bot) { + return interaction.reply({ + content: 'You cannot transfer coins to a bot account.', + ephemeral: true, + }); + } + + if (target.id === interaction.user.id) { + return interaction.reply({ + content: 'You cannot transfer coins to yourself.', + ephemeral: true, + }); + } + + const senderId = interaction.user.id; + const senderData = await collection.findOne({ userID: senderId }); + const senderBalance = senderData?.balance ?? 0; + + if (senderBalance < amount) { + return interaction.reply({ + content: `You only have **${senderBalance}** coins available.`, + ephemeral: true, + }); + } + + await collection.updateOne( + { userID: senderId }, + { $inc: { balance: -amount } }, + { upsert: true } + ); + + await collection.updateOne( + { userID: target.id }, + { $inc: { balance: amount } }, + { upsert: true } + ); + + return interaction.reply({ + content: `You sent **${amount}** coins to ${target.tag}.`, + }); + } +}; diff --git a/commands/utility/about.js b/commands/utility/about.js index c809cd3..723c24c 100644 --- a/commands/utility/about.js +++ b/commands/utility/about.js @@ -1,19 +1,23 @@ -const {SlashCommandBuilder, EmbedBuilder} = require("discord.js"); +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); module.exports = { data: new SlashCommandBuilder() - .setName("about") - .setDescription("Information about this bot and creator"), + .setName('about') + .setDescription('Information about this bot and creator'), async execute(interaction) { const exampleEmbed = new EmbedBuilder() .setColor(0x0099FF) - .setTitle('About Page') - .setDescription('I build this bot for managing my servers. (to lazy to write mor text rn)') + .setTitle('About This Bot') + .setDescription('A multipurpose Discord bot focused on helpful utilities, moderation, and fun community features.') + .addFields( + { name: 'Highlights', value: 'Timezone converter, quiz games, and a growing currency system.' }, + { name: 'Created By', value: interaction.client.user?.username ?? 'Bot Creator' } + ) .setImage('https://i.postimg.cc/QdZZwKVD/Screenshot-2024-12-25-005933.png') .setTimestamp(); await interaction.reply({ embeds: [exampleEmbed] - }) + }); } -} \ No newline at end of file +}; diff --git a/commands/utility/help.js b/commands/utility/help.js index 9936a41..14a7cfe 100644 --- a/commands/utility/help.js +++ b/commands/utility/help.js @@ -1,23 +1,27 @@ -const { SlashCommandBuilder} = require("discord.js"); +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); module.exports = { data: new SlashCommandBuilder() .setName('help') .setDescription('Gives all available commands.'), async execute(interaction) { - // Map over the full command objects to get names and descriptions - const commandDetails = interaction.client.commands.map(cmd => ({ - name: cmd.data.name, - description: cmd.data.description, - })); + const commandDetails = interaction.client.commands + .map(cmd => ({ + name: cmd.data.name, + description: cmd.data.description, + })) + .sort((a, b) => a.name.localeCompare(b.name)); - // Format the commands into a user-friendly help message const helpMessage = commandDetails - .map(detail => `/${detail.name} - ${detail.description}`) + .map(detail => `• **/${detail.name}** — ${detail.description}`) .join('\n'); - // Send the help message - await interaction.reply(`Here are all the available commands:\n\n${helpMessage}`); + const embed = new EmbedBuilder() + .setColor(0x4b7bec) + .setTitle('Available Commands') + .setDescription(helpMessage) + .setFooter({ text: `${commandDetails.length} commands loaded.` }); + await interaction.reply({ embeds: [embed], ephemeral: true }); }, -}; \ No newline at end of file +}; diff --git a/commands/utility/ping.js b/commands/utility/ping.js index 2b6a058..c694e32 100644 --- a/commands/utility/ping.js +++ b/commands/utility/ping.js @@ -3,8 +3,15 @@ const { SlashCommandBuilder } = require('discord.js'); module.exports = { data: new SlashCommandBuilder() .setName('ping') - .setDescription('Check if the bots responsivnes'), + .setDescription('Check the bot\'s responsiveness.'), async execute(interaction) { - await interaction.reply({ content: 'Pong! (Online)', ephemeral: true }); + const sentAt = Date.now(); + await interaction.reply({ content: 'Pinging...', ephemeral: true }); + const latency = Date.now() - sentAt; + const apiLatency = Math.round(interaction.client.ws.ping); + + await interaction.editReply({ + content: `Pong! **${latency}ms** (API: **${apiLatency}ms**)`, + }); }, -}; \ No newline at end of file +}; From 10b33a79b399329df8eb0716941e39e30776e6f4 Mon Sep 17 00:00:00 2001 From: fretuks Date: Mon, 26 Jan 2026 12:15:25 +0100 Subject: [PATCH 08/10] Small fix --- commands/db/balance.js | 0 events/tempban.js | 66 +++++++++++++++++++++++++----------------- 2 files changed, 39 insertions(+), 27 deletions(-) delete mode 100644 commands/db/balance.js diff --git a/commands/db/balance.js b/commands/db/balance.js deleted file mode 100644 index e69de29..0000000 diff --git a/events/tempban.js b/events/tempban.js index dd2e9ab..9a038f6 100644 --- a/events/tempban.js +++ b/events/tempban.js @@ -1,5 +1,5 @@ -const { Events } = require('discord.js'); -const { connectToDatabase } = require('../db.js'); +const {Events} = require('discord.js'); +const {connectToDatabase} = require('../db.js'); module.exports = { name: Events.ClientReady, @@ -7,38 +7,50 @@ module.exports = { const dbClient = await connectToDatabase(); const db = dbClient.db('discord'); const tempBanCollection = db.collection('tempban'); + const scheduled = new Map(); + async function unbanNow(banData) { + try { + const guild = await client.guilds.fetch(banData.guildId); + await guild.bans.remove(banData.userId); + await tempBanCollection.deleteOne({_id: banData._id}); + } catch (err) { + console.error('Error unbanning member:', err); + } finally { + scheduled.delete(String(banData._id)); + } + } - // 2) Define a function to schedule unbans - async function scheduleUnban(banData) { - const delay = banData.banTime - Date.now(); + function scheduleUnban(banData) { + const id = String(banData._id); + if (scheduled.has(id)) return; + const delay = banData.banTime - Date.now(); // assumes banTime is a future timestamp (ms) if (delay <= 0) { + // already expired -> unban immediately + void unbanNow(banData); return; } - setTimeout(async () => { - try { - const guild = await client.guilds.fetch(banData.guildId); - await guild.bans.remove(banData.userId); - await tempBanCollection.deleteOne({ _id: banData._id }); - } catch (err) { - console.error('Error unbanning member:', err); - } - }, delay); + const timeoutId = setTimeout(() => void unbanNow(banData), delay); + scheduled.set(id, timeoutId); } - try { - const allBans = await tempBanCollection.find({}).toArray(); - allBans.forEach(scheduleUnban); - - tempBanCollection.watch().on('change', (change) => { - if (change.operationType === 'insert') { - const newBan = change.fullDocument; - scheduleUnban(newBan); - } - }); - } catch (err) { - console.error('Error setting up unban system:', err); + async function scanAndSchedule() { + try { + const now = Date.now(); + const futureBans = await tempBanCollection.find({banTime: {$gt: now}}).toArray(); + futureBans.forEach(scheduleUnban); + const expired = await tempBanCollection.find({banTime: {$lte: now}}).toArray(); + expired.forEach((b) => void unbanNow(b)); + } catch (err) { + console.error('Error scanning tempbans:', err); + } } + + // Initial load + await scanAndSchedule(); + + // Poll every 10 seconds for newly inserted bans + setInterval(scanAndSchedule, 10_000); }, -}; +}; \ No newline at end of file From 1ace32dd003540ea39941ec1e4a86e6b452de343 Mon Sep 17 00:00:00 2001 From: fretuks Date: Mon, 26 Jan 2026 13:53:06 +0100 Subject: [PATCH 09/10] Ignore config.json --- .gitignore | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 From a41a81f36774dbcd7bb88092163d49774e4e74b0 Mon Sep 17 00:00:00 2001 From: fretuks Date: Mon, 26 Jan 2026 13:54:20 +0100 Subject: [PATCH 10/10] Ignore secrets and local files --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index e69de29..b6d67a8 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,4 @@ +config.json +.env +node_modules/ +.idea/