From ef7ca16a853ba6be6f567edc86ba328c1c276001 Mon Sep 17 00:00:00 2001 From: Fretux Date: Mon, 26 Jan 2026 14:10:46 +0100 Subject: [PATCH] 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] }); + }, +};