From b10f3887ed45c6050b88e82733e1eef9cae37172 Mon Sep 17 00:00:00 2001 From: ScriptSorcerer20 Date: Fri, 24 Jan 2025 14:23:24 +0100 Subject: [PATCH 1/8] 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 7cb357e61d29c591e3c8b4e318d7330d0ae14da1 Mon Sep 17 00:00:00 2001 From: ScriptSorcerer20 Date: Sat, 25 Jan 2025 16:56:46 +0100 Subject: [PATCH 2/8] 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 c913661109eb5e68b52431de64f5b7c84b81b4c2 Mon Sep 17 00:00:00 2001 From: ScriptSorcerer20 Date: Tue, 28 Jan 2025 22:40:05 +0100 Subject: [PATCH 3/8] 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 8fd4ed237b7ea03d30659a88510b452885c2b2c2 Mon Sep 17 00:00:00 2001 From: ScriptSorcerer20 Date: Thu, 30 Jan 2025 20:15:50 +0100 Subject: [PATCH 4/8] 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 329d3a131de19237c9e3436dada56e386dd33bea Mon Sep 17 00:00:00 2001 From: ScriptSorcerer20 Date: Wed, 12 Mar 2025 19:35:49 +0100 Subject: [PATCH 5/8] 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 8483c490cb69e9f82780d8e2fd069d3c3bf9e178 Mon Sep 17 00:00:00 2001 From: Fretux Date: Mon, 26 Jan 2026 11:34:48 +0100 Subject: [PATCH 6/8] 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 0c369606459f612290a1807f2ccd787553425197 Mon Sep 17 00:00:00 2001 From: Fretux Date: Mon, 26 Jan 2026 12:14:18 +0100 Subject: [PATCH 7/8] 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 ef7ca16a853ba6be6f567edc86ba328c1c276001 Mon Sep 17 00:00:00 2001 From: Fretux Date: Mon, 26 Jan 2026 14:10:46 +0100 Subject: [PATCH 8/8] Add wiki and weather commands --- README.md | 26 ++++++------ commands/db/balance.js | 1 + commands/db/earn.js | 1 + commands/db/give.js | 1 + commands/db/test.js | 1 + commands/db/transfer.js | 1 + commands/moderation/kick.js | 11 ++++- commands/moderation/mute.js | 34 ++++++++++------ commands/moderation/temp_ban.js | 8 ++-- commands/unique/quiz.js | 3 +- commands/unique/timestamp.js | 3 +- commands/utility/about.js | 3 +- commands/utility/help.js | 23 +++++++++-- commands/utility/ping.js | 16 ++++++-- commands/utility/server.js | 26 ++++++++++-- commands/utility/user.js | 30 ++++++++++++-- commands/utility/weather.js | 72 +++++++++++++++++++++++++++++++++ commands/utility/wiki.js | 54 +++++++++++++++++++++++++ 18 files changed, 266 insertions(+), 48 deletions(-) create mode 100644 commands/utility/weather.js create mode 100644 commands/utility/wiki.js diff --git a/README.md b/README.md index 753474b..d8ddd1a 100644 --- a/README.md +++ b/README.md @@ -6,16 +6,16 @@ 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** - - [x] Add a `!help` command to display a list of available commands. - - [x] Implement a `!ping` command to check bot responsiveness. - - [ **Only Text missing** ] 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. + - [x] Add an `/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, x kick, and x temp_ban users. + - [x] Add commands to mute, kick, and temp_ban users. - [ ] Create an auto-moderation feature to delete spam or inappropriate content. - [ ] Implement a warning system with a strike counter. @@ -23,23 +23,23 @@ Im going to make a To-do List for the features i want to implement for my Bot. #### **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. + - [x] Design a virtual currency system with basic commands: + - [x] `/balance` to check currency balance. + - [x] `/earn` to earn daily or periodic rewards. + - [x] `/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** (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. + - [x] `/timestamp [date]` to generate Discord timestamps. + - [x] Support multiple Discord timestamp 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. + - [x] 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]`). @@ -49,8 +49,8 @@ Im going to make a To-do List for the features i want to implement for my Bot. #### **Utility Tools** 1. **Search and Lookups** - - [ ] Add a `!wiki [topic]` command to fetch summaries from Wikipedia. - - [ ] Include a `!weather [location]` command for weather updates. + - [x] Add a `/wiki [topic]` command to fetch summaries from Wikipedia. + - [x] Include a `/weather [location]` command for weather updates. --- diff --git a/commands/db/balance.js b/commands/db/balance.js index 32b274e..f6d352e 100644 --- a/commands/db/balance.js +++ b/commands/db/balance.js @@ -10,6 +10,7 @@ module.exports = { .setName('user') .setDescription('User to check (defaults to you)') .setRequired(false)), + category: 'Currency', async execute(interaction) { const dbClient = await connectToDatabase(); const db = dbClient.db('discord'); diff --git a/commands/db/earn.js b/commands/db/earn.js index 6853b1e..b2ff060 100644 --- a/commands/db/earn.js +++ b/commands/db/earn.js @@ -22,6 +22,7 @@ module.exports = { data: new SlashCommandBuilder() .setName('earn') .setDescription('Claim your daily coin reward.'), + category: 'Currency', async execute(interaction) { const dbClient = await connectToDatabase(); const db = dbClient.db('discord'); diff --git a/commands/db/give.js b/commands/db/give.js index 77a6548..f8dca2e 100644 --- a/commands/db/give.js +++ b/commands/db/give.js @@ -17,6 +17,7 @@ module.exports = { .setName('target') .setDescription('User to receive coins') .setRequired(true)), + category: 'Currency', async execute(interaction) { const dbClient = await connectToDatabase(); const target = interaction.options.getUser('target'); diff --git a/commands/db/test.js b/commands/db/test.js index 3adc425..937f368 100644 --- a/commands/db/test.js +++ b/commands/db/test.js @@ -11,6 +11,7 @@ module.exports = { .setName('message') .setDescription('string to store') .setRequired(true)), + category: 'Database', async execute(interaction) { const dbClient = await connectToDatabase(); const message = interaction.options.getString('message'); diff --git a/commands/db/transfer.js b/commands/db/transfer.js index b2adda2..dab7bdf 100644 --- a/commands/db/transfer.js +++ b/commands/db/transfer.js @@ -16,6 +16,7 @@ module.exports = { .setDescription('Amount of coins to send') .setMinValue(1) .setRequired(true)), + category: 'Currency', async execute(interaction) { const dbClient = await connectToDatabase(); const db = dbClient.db('discord'); diff --git a/commands/moderation/kick.js b/commands/moderation/kick.js index 00d19e8..1971922 100644 --- a/commands/moderation/kick.js +++ b/commands/moderation/kick.js @@ -1,9 +1,10 @@ -const { SlashCommandBuilder } = require("discord.js"); +const { SlashCommandBuilder, PermissionFlagsBits } = require("discord.js"); module.exports = { data: new SlashCommandBuilder() .setName("kick") .setDescription("Kick misbehaving people") + .setDefaultMemberPermissions(PermissionFlagsBits.KickMembers) .addUserOption(option => option .setName('target') @@ -13,6 +14,7 @@ module.exports = { option .setName('reason') .setDescription('The reason of the kick')), + category: 'Moderation', async execute(interaction) { const user = interaction.options.getUser('target'); const reason = interaction.options.getString('reason') || 'No reason provided'; @@ -20,6 +22,13 @@ module.exports = { try { const member = await interaction.guild.members.fetch(user.id); + if (!member.kickable) { + return interaction.reply({ + content: 'I cannot kick that member. Check my role permissions and hierarchy.', + ephemeral: true, + }); + } + await member.kick(reason); await interaction.reply(`Kicked ${user.tag} for: ${reason}`); diff --git a/commands/moderation/mute.js b/commands/moderation/mute.js index 09d56a8..fd53b0b 100644 --- a/commands/moderation/mute.js +++ b/commands/moderation/mute.js @@ -1,9 +1,10 @@ -const { SlashCommandBuilder } = require("discord.js"); +const { SlashCommandBuilder, PermissionFlagsBits } = require("discord.js"); module.exports = { data: new SlashCommandBuilder() .setName("mute") - .setDescription("mute misbehaving people") + .setDescription("Temporarily mute a member with a timeout.") + .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers) .addUserOption(option => option .setName('target') @@ -12,27 +13,34 @@ module.exports = { .addIntegerOption(option => option .setName('duration') - .setDescription('The duration of the Mute (Minutes)') + .setDescription('The duration of the mute (in minutes)') + .setMinValue(1) + .setMaxValue(40320) .setRequired(true)) .addStringOption(option => option .setName('reason') .setDescription('The reason of the mute')), + category: 'Moderation', async execute(interaction) { const target = interaction.options.getUser('target'); const duration = interaction.options.getInteger('duration'); - const reason = interaction.options.getString('reason'); + const reason = interaction.options.getString('reason') || 'No reason provided'; - // Convert duration to milliseconds const durationMs = duration * 60 * 1000; + const member = await interaction.guild.members.fetch(target.id); - const guildmember = await interaction.guild.members.fetch(target.id); - - const muteRole = interaction.guild.roles.find(role => role.name === "mute"); - if (!muteRole) { - - } else { - await guildmember.roles.add(muteRole, reason); + if (!member.moderatable) { + return interaction.reply({ + content: 'I cannot mute that member. Check my role permissions and hierarchy.', + ephemeral: true, + }); } + + await member.timeout(durationMs, reason); + + return interaction.reply({ + content: `Muted ${target.tag} for **${duration} minutes**. Reason: ${reason}`, + }); }, -}; \ No newline at end of file +}; diff --git a/commands/moderation/temp_ban.js b/commands/moderation/temp_ban.js index 871e467..cc4b87f 100644 --- a/commands/moderation/temp_ban.js +++ b/commands/moderation/temp_ban.js @@ -1,15 +1,16 @@ -const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); +const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require("discord.js"); const { connectToDatabase } = require('../../db.js'); module.exports = { mod: true, data: new SlashCommandBuilder() .setName("temp-ban") - .setDescription("Kick misbehaving people") + .setDescription("Temporarily ban a member.") + .setDefaultMemberPermissions(PermissionFlagsBits.BanMembers) .addUserOption(option => option .setName('target') - .setDescription('The member to kick') + .setDescription('The member to ban') .setRequired(true)) .addIntegerOption(option => option @@ -26,6 +27,7 @@ module.exports = { .setName('delete-messages') .setDescription('Number of days to delete messages') .setMinValue(0).setMaxValue(7)), + category: 'Moderation', async execute(interaction) { const user = interaction.options.getUser('target'); const reason = interaction.options.getString('reason'); diff --git a/commands/unique/quiz.js b/commands/unique/quiz.js index 6956d7f..7bbccab 100644 --- a/commands/unique/quiz.js +++ b/commands/unique/quiz.js @@ -5,6 +5,7 @@ module.exports = { data: new SlashCommandBuilder() .setName("quiz") .setDescription("Start a cool quiz"), + category: 'Games', async execute(interaction) { const item = quiz[Math.floor(Math.random() * quiz.length)]; await interaction.reply({ @@ -42,4 +43,4 @@ module.exports = { ); } }, -}; \ No newline at end of file +}; diff --git a/commands/unique/timestamp.js b/commands/unique/timestamp.js index 01f2b57..b63a014 100644 --- a/commands/unique/timestamp.js +++ b/commands/unique/timestamp.js @@ -25,6 +25,7 @@ module.exports = { { name: 'Full date and time with weekday', value: 'F' }, { name: 'Relative time', value: 'R' }, )), + category: 'Utility', async execute(interaction) { // Get user input const dateInput = interaction.options.getString('date'); @@ -57,4 +58,4 @@ module.exports = { }); } }, -}; \ No newline at end of file +}; diff --git a/commands/utility/about.js b/commands/utility/about.js index 723c24c..55135ad 100644 --- a/commands/utility/about.js +++ b/commands/utility/about.js @@ -4,13 +4,14 @@ module.exports = { data: new SlashCommandBuilder() .setName('about') .setDescription('Information about this bot and creator'), + category: 'Utility', async execute(interaction) { const exampleEmbed = new EmbedBuilder() .setColor(0x0099FF) .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: 'Highlights', value: 'Timezone converter, quiz games, a currency system, and helpful utility tools.' }, { name: 'Created By', value: interaction.client.user?.username ?? 'Bot Creator' } ) .setImage('https://i.postimg.cc/QdZZwKVD/Screenshot-2024-12-25-005933.png') diff --git a/commands/utility/help.js b/commands/utility/help.js index 14a7cfe..e1b63f1 100644 --- a/commands/utility/help.js +++ b/commands/utility/help.js @@ -4,22 +4,37 @@ module.exports = { data: new SlashCommandBuilder() .setName('help') .setDescription('Gives all available commands.'), + category: 'Utility', async execute(interaction) { const commandDetails = interaction.client.commands .map(cmd => ({ name: cmd.data.name, description: cmd.data.description, + category: cmd.category ?? 'Other', })) .sort((a, b) => a.name.localeCompare(b.name)); - const helpMessage = commandDetails - .map(detail => `β€’ **/${detail.name}** β€” ${detail.description}`) - .join('\n'); + const grouped = commandDetails.reduce((acc, command) => { + const group = acc[command.category] ?? []; + group.push(command); + acc[command.category] = group; + return acc; + }, {}); + + const fields = Object.keys(grouped) + .sort((a, b) => a.localeCompare(b)) + .map((category) => ({ + name: category, + value: grouped[category] + .map(command => `β€’ **/${command.name}** β€” ${command.description}`) + .join('\n'), + })); const embed = new EmbedBuilder() .setColor(0x4b7bec) .setTitle('Available Commands') - .setDescription(helpMessage) + .setDescription('Here is everything I can do right now.') + .addFields(fields) .setFooter({ text: `${commandDetails.length} commands loaded.` }); await interaction.reply({ embeds: [embed], ephemeral: true }); diff --git a/commands/utility/ping.js b/commands/utility/ping.js index c694e32..f7ed4b1 100644 --- a/commands/utility/ping.js +++ b/commands/utility/ping.js @@ -1,17 +1,25 @@ -const { SlashCommandBuilder } = require('discord.js'); +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); module.exports = { data: new SlashCommandBuilder() .setName('ping') .setDescription('Check the bot\'s responsiveness.'), + category: 'Utility', async execute(interaction) { 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**)`, - }); + const embed = new EmbedBuilder() + .setColor(0x34ace0) + .setTitle('Pong! πŸ“') + .addFields( + { name: 'Round Trip', value: `${latency}ms`, inline: true }, + { name: 'API Latency', value: `${apiLatency}ms`, inline: true }, + ) + .setTimestamp(); + + await interaction.editReply({ embeds: [embed], content: null }); }, }; diff --git a/commands/utility/server.js b/commands/utility/server.js index d4027ca..8839476 100644 --- a/commands/utility/server.js +++ b/commands/utility/server.js @@ -1,10 +1,30 @@ -const { SlashCommandBuilder } = require('discord.js'); +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); module.exports = { data: new SlashCommandBuilder() .setName('server') .setDescription('Provides information about the server.'), + category: 'Utility', async execute(interaction) { - await interaction.reply(`This server is ${interaction.guild.name} and has ${interaction.guild.memberCount} members.`); + const { guild } = interaction; + const owner = await guild.fetchOwner(); + const created = ``; + const boostCount = guild.premiumSubscriptionCount ?? 0; + + const embed = new EmbedBuilder() + .setColor(0x2ecc71) + .setTitle(guild.name) + .setThumbnail(guild.iconURL({ size: 256 })) + .addFields( + { name: 'Server ID', value: guild.id, inline: true }, + { name: 'Owner', value: owner.user.tag, inline: true }, + { name: 'Members', value: guild.memberCount.toString(), inline: true }, + { name: 'Boosts', value: boostCount.toString(), inline: true }, + { name: 'Created', value: created, inline: false }, + ) + .setFooter({ text: `Requested by ${interaction.user.tag}` }) + .setTimestamp(); + + await interaction.reply({ embeds: [embed], ephemeral: true }); }, -}; \ No newline at end of file +}; diff --git a/commands/utility/user.js b/commands/utility/user.js index 997b538..d52a5fe 100644 --- a/commands/utility/user.js +++ b/commands/utility/user.js @@ -1,10 +1,32 @@ -const { SlashCommandBuilder } = require('discord.js'); +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); module.exports = { data: new SlashCommandBuilder() .setName('user') - .setDescription('Provides information about the user.'), + .setDescription('Provides information about a user.') + .addUserOption(option => + option + .setName('target') + .setDescription('User to look up') + .setRequired(false)), + category: 'Utility', async execute(interaction) { - await interaction.reply(`This command was run by ${interaction.user.username}, who joined on ${interaction.member.joinedAt}.`); + const target = interaction.options.getUser('target') ?? interaction.user; + const member = await interaction.guild.members.fetch(target.id); + + const embed = new EmbedBuilder() + .setColor(0x4b7bec) + .setTitle(`${target.username}`) + .setThumbnail(target.displayAvatarURL({ size: 256 })) + .addFields( + { name: 'Tag', value: target.tag, inline: true }, + { name: 'User ID', value: target.id, inline: true }, + { name: 'Account Created', value: `` }, + { name: 'Joined Server', value: member.joinedAt ? `` : 'Unknown' }, + ) + .setFooter({ text: `Requested by ${interaction.user.tag}` }) + .setTimestamp(); + + await interaction.reply({ embeds: [embed], ephemeral: true }); }, -}; \ No newline at end of file +}; diff --git a/commands/utility/weather.js b/commands/utility/weather.js new file mode 100644 index 0000000..f65f063 --- /dev/null +++ b/commands/utility/weather.js @@ -0,0 +1,72 @@ +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); + +const getJson = async (url) => { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Request failed with status ${response.status}`); + } + return response.json(); +}; + +const formatTemperature = (value, unit) => `${Math.round(value)}Β°${unit === 'fahrenheit' ? 'F' : 'C'}`; + +module.exports = { + data: new SlashCommandBuilder() + .setName('weather') + .setDescription('Get the current weather for a location.') + .addStringOption(option => + option + .setName('location') + .setDescription('City or place name to look up') + .setRequired(true)) + .addStringOption(option => + option + .setName('units') + .setDescription('Temperature units') + .setRequired(false) + .addChoices( + { name: 'Celsius', value: 'celsius' }, + { name: 'Fahrenheit', value: 'fahrenheit' }, + )), + category: 'Utility', + async execute(interaction) { + const location = interaction.options.getString('location'); + const units = interaction.options.getString('units') ?? 'celsius'; + + await interaction.deferReply(); + + try { + const geoUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(location)}&count=1&language=en&format=json`; + const geo = await getJson(geoUrl); + const match = geo.results?.[0]; + + if (!match) { + return interaction.editReply({ content: 'I could not find that location. Try a more specific place name.' }); + } + + const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${match.latitude}&longitude=${match.longitude}¤t_weather=true&temperature_unit=${units}`; + const weather = await getJson(weatherUrl); + const current = weather.current_weather; + + if (!current) { + return interaction.editReply({ content: 'Weather data is currently unavailable for that location.' }); + } + + const embed = new EmbedBuilder() + .setColor(0x3498db) + .setTitle(`Weather for ${match.name}${match.country ? `, ${match.country}` : ''}`) + .addFields( + { name: 'Temperature', value: formatTemperature(current.temperature, units), inline: true }, + { name: 'Wind Speed', value: `${Math.round(current.windspeed)} km/h`, inline: true }, + { name: 'Conditions', value: `Weather code: ${current.weathercode}`, inline: true }, + ) + .setFooter({ text: `Latitude ${match.latitude.toFixed(2)}, Longitude ${match.longitude.toFixed(2)}` }) + .setTimestamp(new Date(current.time)); + + return interaction.editReply({ embeds: [embed] }); + } catch (error) { + console.error(error); + return interaction.editReply({ content: 'Something went wrong fetching the weather. Please try again later.' }); + } + }, +}; diff --git a/commands/utility/wiki.js b/commands/utility/wiki.js new file mode 100644 index 0000000..91b5844 --- /dev/null +++ b/commands/utility/wiki.js @@ -0,0 +1,54 @@ +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); + +const MAX_DESCRIPTION = 4096; + +const truncate = (text, maxLength) => { + if (!text) return 'No summary available.'; + if (text.length <= maxLength) return text; + return `${text.slice(0, maxLength - 3)}...`; +}; + +module.exports = { + data: new SlashCommandBuilder() + .setName('wiki') + .setDescription('Search Wikipedia for a topic.') + .addStringOption(option => + option + .setName('topic') + .setDescription('Topic to search') + .setRequired(true)), + category: 'Utility', + async execute(interaction) { + const topic = interaction.options.getString('topic'); + const endpoint = `https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(topic)}`; + + await interaction.deferReply(); + + const response = await fetch(endpoint, { + headers: { + 'User-Agent': 'DiscordBot/1.0 (https://github.com)', + }, + }); + + if (!response.ok) { + return interaction.editReply({ + content: 'I could not find a Wikipedia summary for that topic.', + }); + } + + const data = await response.json(); + + const embed = new EmbedBuilder() + .setColor(0x8e44ad) + .setTitle(data.title ?? topic) + .setURL(data.content_urls?.desktop?.page ?? `https://en.wikipedia.org/wiki/${encodeURIComponent(topic)}`) + .setDescription(truncate(data.extract, MAX_DESCRIPTION)) + .setFooter({ text: 'Source: Wikipedia' }); + + if (data.thumbnail?.source) { + embed.setThumbnail(data.thumbnail.source); + } + + return interaction.editReply({ embeds: [embed] }); + }, +};