diff --git a/src/commands/admin/announce.js b/src/commands/admin/announce.js new file mode 100644 index 000000000..d4014c0c0 --- /dev/null +++ b/src/commands/admin/announce.js @@ -0,0 +1,173 @@ +const { + EmbedBuilder, + ApplicationCommandOptionType, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + ComponentType, +} = require("discord.js"); + +module.exports = { + name: "announce", + description: "Create an announcement in a specified channel & pings @everyone", + cooldown: 0, + category: "MODERATION", + memberpermissions: ['ADMINISTRATOR'], + aliases: ["a"], + usage: "[CHANNEL] [TITLE] [TEXT]", + minArgsCount: 3, + command: { + enabled: true, + }, + slashCommand: { + enabled: true, + ephemeral: false, + options: [ + { + name: "channel", + description: "The channel to post the announcement in", + type: 7, // CHANNEL + required: true, + }, + { + name: "title", + description: "The title of the announcement", + type: 3, // STRING + required: true, + }, + { + name: "text", + description: "The text of the announcement(put backwords / n without the space to do line breaks)", + type: 3, // STRING, + required: true, + }, + { + name: "color", + description: "The color of the announcement embed (in hex code format)", + type: 3, // STRING + required: false, + }, + { + name: "image", + description: "The URL of the image to include in the announcement embed", + type: 3, // STRING, + required: false, + }, + { + name: "footer", + description: "The text to display in the footer of the announcement embed", + type: 3, // STRING, + required: false, + }, + { + name: "message_id", + description: "The ID of the announcement message to edit", + type: 3, // STRING + required: false, + }, + ], + }, + +async messageRun(message, args, data) { + // Check if the command has enough arguments + if (args.length < 3) { + return message.channel.send("Missing required arguments: [CHANNEL] [TITLE] [TEXT]"); + } + // Get the channel, title, and text from the command arguments + const channel = message.mentions.channels.first(); + const title = args[0]; + const image = args[1]; + const text = args.slice(2).join(" "); + const color = "#0099ff"; + const footer = args[3] || ""; + const messageId = args[4]; + + // Create an embed with the announcement information and color + const embed = new EmbedBuilder() + .setColor(color) + .setTitle(title) + .setDescription(text.replace(/\\n/g, "\n")); + + // Add the image to the embed, if provided + if (image) { + embed.setImage(image); + } + + // Add the footer to the embed, if provided + if (footer) { + embed.setFooter({ text: footer }); + } + + // If messageId is provided, edit the existing message + if (messageId) { + try { + const message = await channel.messages.fetch(messageId); + await message.edit({ embeds: [embed] }); + await message.channel.send({ content: "@everyone", allowedMentions: { parse: ["everyone"] } }); + await message.channel.send(`Announcement embed edited in ${channel}`); + } catch (err) { + console.error(err); + await message.channel.send(`Failed to edit announcement embed in ${channel}`); + } + } else { + // If messageId is not provided, send a new message + const sentMessage = await channel.send({ embeds: [embed] }); + await channel.send({ content: "@everyone", allowedMentions: { parse: ["everyone"] } }); + await message.channel.send(`Announcement embed sent in ${channel}`); + } +}, + +async interactionRun(interaction, data) { + // Get the channel, title, and text from the slash command + const channel = interaction.options.getChannel("channel"); + const title = interaction.options.getString("title"); + const image = interaction.options.getString("image"); + const text = interaction.options.getString("text"); + const color = interaction.options.getString("color") || "#0099ff"; + const footer = interaction.options.getString("footer"); + const messageId = interaction.options.getString("message_id"); + + + // Create an embed with the announcement information and color + const embed = new EmbedBuilder() + .setColor(color) + .setTitle(title) + .setDescription(text.replace(/\\n/g, "\n")); + + // Add the image to the embed, if provided + if (image) { + embed.setImage(image); + } + + // Add the footer to the embed, if provided + if (footer) { + embed.setFooter({ text: footer }); + } + + // If message_id is provided, edit the existing message + if (messageId) { + try { + const message = await channel.messages.fetch(messageId); + await message.edit({ embeds: [embed] }); + await interaction.followUp({ + content: `Announcement embed edited in ${channel}`, + ephemeral: false, + }); + } catch (err) { + console.error(err); + await interaction.followUp({ + content: `Failed to edit announcement embed in ${channel}`, + ephemeral: true, + }); + } + } else { + // If message_id is not provided, send a new message + const sentMessage = await channel.send({ embeds: [embed] }); + await channel.send({ content: "@everyone", allowedMentions: { parse: ["everyone"] } }); + await interaction.followUp({ + content: `Announcement embed sent in ${channel}`, + ephemeral: false, + }); + } +} +}; diff --git a/src/commands/admin/say.js b/src/commands/admin/say.js new file mode 100644 index 000000000..d6ade5c59 --- /dev/null +++ b/src/commands/admin/say.js @@ -0,0 +1,129 @@ +const { + EmbedBuilder, + ApplicationCommandOptionType, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + ComponentType, +} = require("discord.js"); + +module.exports = { + name: "say", + description: "Says a message as the bot to a channel you choose", + category: "ADMIN", + botPermissions: ["SendMessages"], + userPermissions: ["ManageMessages"], + slashCommand: { + enabled: true, + ephemeral: true, + description: "Says a message as the bot to a channel you choose", + options: [ + { + name: "message", + description: "The message to be sent.", + type: 3, + required: true, + }, + { + name: "channel", + description: "The channel where the message will be sent.", + type: 7, + required: false, + }, + { + name: "message_id", + description: "The ID of the message to edit or reply to.", + type: 3, + required: false, + }, + { + name: "edit", + description: "Whether to edit the message specified by message_id instead of sending a new message.", + type: 5, + required: false, + }, + { + name: "ping", + description: "Whether to ping everyone in the channel after sending the message.", + type: 5, + required: false, + }, + ], + }, +async execute(interaction) { + const { options } = interaction; + + // Retrieve the message content + const message = options.getString("message").replace(/\\n/g, '\n'); + + // Retrieve the channel where the message will be sent + const channel = options.getChannel("channel") || interaction.channel; + + // Retrieve the message ID to edit or reply to + const message_id = options.getString("message_id"); + + // Retrieve whether to edit the message specified by message_id + const edit = options.getBoolean("edit"); + + // Retrieve whether to ping everyone in the channel after sending the message + const ping = options.getBoolean("ping"); + + try { + // If a message ID is provided, retrieve the message and edit or reply to it + if (message_id) { + const replyMessage = await channel.messages.fetch(message_id).catch(() => null); + + if (!replyMessage) { + await interaction.followUp({ content: "Invalid message ID.", ephemeral: true }); + } + + if (edit) { + await replyMessage.edit(message); + } else { + await replyMessage.reply({ content: `${message}\n${ping ? "@everyone" : ""}`, allowedMentions: { parse: ["everyone", "roles", "users"] } }); + } + + // Send the final reply + await interaction.followUp({ content: edit ? "Message edited" : "Message sent", ephemeral: true }); + } else { + // If no message ID is provided, send a new message + const taggedChannel = options.getChannel("channel"); + +if (taggedChannel) { + await taggedChannel.send({ content: message, allowedMentions: { parse: ["everyone", "roles", "users"] } }); + if (ping) { + setTimeout(async () => { + await taggedChannel.send({ content: "@everyone", allowedMentions: { parse: ["everyone", "roles", "users"] } }); + }, 2000); // wait 2 seconds before sending the second message + } +} else { + await interaction.channel.send({ content: message, allowedMentions: { parse: ["everyone", "roles", "users"] } }); + if (ping) { + setTimeout(async () => { + await interaction.channel.send({ content: "@everyone", allowedMentions: { parse: ["everyone", "roles", "users"] } }); + }, 2000); // wait 2 seconds before sending the second message + } +} + + + // Send the final reply + await interaction.followUp({ content: "Message sent", ephemeral: true }); + } + } catch (error) { + console.error(error); + await interaction.followUp({ content: "An error occurred while processing this command.", ephemeral: true }); + } +}, + + async messageRun(message, args, data) { + const replyEmbed = new EmbedBuilder() + .setTitle("Command Deprecated") + .setDescription("Please use the slash command instead.\n\n**Usage:** /say [channel] [message_id] [edit] [ping]"); + + return message.reply({ embeds: [replyEmbed], ephemeral: true }); +}, + + async interactionRun(interaction) { + await this.execute(interaction); + }, +}; diff --git a/src/commands/ticket/ticket.js b/src/commands/ticket/ticket.js index 0f659f142..880fa6cb3 100644 --- a/src/commands/ticket/ticket.js +++ b/src/commands/ticket/ticket.js @@ -12,7 +12,6 @@ const { } = require("discord.js"); const { EMBED_COLORS } = require("@root/config.js"); const { isTicketChannel, closeTicket, closeAllTickets } = require("@handlers/ticket"); - /** * @type {import("@structures/Command")} */ @@ -53,6 +52,10 @@ module.exports = { trigger: "remove ", description: "remove user/role from the ticket", }, + { + trigger: "category ", + description: "set the ticket category channel by ID", + }, ], }, slashCommand: { @@ -135,6 +138,24 @@ module.exports = { }, ], }, + { + name: "category", + description: "set the ticket category channel by ID", + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: "id", + description: "the ID of the category channel to set as the ticket category", + type: ApplicationCommandOptionType.String, + required: true, + }, + ], + }, + { + name: "remove-c", + description: "remove the ticket category channel", + type: ApplicationCommandOptionType.Subcommand, + }, ], }, @@ -202,7 +223,11 @@ module.exports = { else inputId = args[1]; response = await removeFromTicket(message, inputId); } - + // Set ticket category + else if (input === "category") { + // Call function to set category channel in guild settings + response = await setTicketCategory(message.guild, args[1], data.settings); + } // Invalid input else { return message.safeReply("Incorrect command usage"); @@ -260,11 +285,42 @@ module.exports = { const user = interaction.options.getUser("user"); response = await removeFromTicket(interaction, user.id); } - + // Set ticket category + else if (sub === "category") { + const categoryId = interaction.options.getString("id"); + response = await setTicketCategory(interaction.guild, categoryId, data.settings); + } + else if (sub === "remove-c") { + response = await removeTicketCategory(interaction.guild, data.settings); + } if (response) await interaction.followUp(response); }, }; +async function removeTicketCategory(guild, settings) { + // Remove the category channel ID from guild settings + settings.ticket.category_channel = null; + await settings.save(); + + return "Ticket category channel removed successfully"; +} +async function setTicketCategory(guild, categoryId, settings) { + + // Find the category channel by ID + const categoryChannel = guild.channels.cache.find( + (channel) => channel.type === 4 && channel.id === categoryId + ); + + // Check if the category channel was found + if (!categoryChannel) { + return "Invalid category ID provided or it does not refer to a category channel"; + } + + // Update the category channel ID in guild settings + settings.ticket.category_channel = categoryId; + await settings.save(); + return "Ticket category channel set successfully"; +} /** * @param {import('discord.js').Message} param0 * @param {import('discord.js').GuildTextBasedChannel} targetChannel @@ -276,7 +332,7 @@ async function ticketModalSetup({ guild, channel, member }, targetChannel, setti ); const sentMsg = await channel.safeSend({ - content: "Please click the button below to setup ticket message", + content: "Please click the button below to setup your ticket system!", components: [buttonRow], }); @@ -301,21 +357,28 @@ async function ticketModalSetup({ guild, channel, member }, targetChannel, setti new ActionRowBuilder().addComponents( new TextInputBuilder() .setCustomId("title") - .setLabel("Embed Title") + .setLabel("ticket Title") .setStyle(TextInputStyle.Short) .setRequired(false) ), new ActionRowBuilder().addComponents( new TextInputBuilder() .setCustomId("description") - .setLabel("Embed Description") + .setLabel("ticket Description") .setStyle(TextInputStyle.Paragraph) .setRequired(false) ), new ActionRowBuilder().addComponents( new TextInputBuilder() .setCustomId("footer") - .setLabel("Embed Footer") + .setLabel("ticket Footer") + .setStyle(TextInputStyle.Short) + .setRequired(false) + ), + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId("staff") + .setLabel("Staff Role (ID separate with ,)") .setStyle(TextInputStyle.Short) .setRequired(false) ), @@ -337,6 +400,10 @@ async function ticketModalSetup({ guild, channel, member }, targetChannel, setti const title = modal.fields.getTextInputValue("title"); const description = modal.fields.getTextInputValue("description"); const footer = modal.fields.getTextInputValue("footer"); + const staffRoles = modal.fields + .getTextInputValue("staff") + .split(",") + .filter((s) => guild.roles.cache.has(s.trim())); // send ticket message const embed = new EmbedBuilder() @@ -349,6 +416,11 @@ async function ticketModalSetup({ guild, channel, member }, targetChannel, setti new ButtonBuilder().setLabel("Open a ticket").setCustomId("TICKET_CREATE").setStyle(ButtonStyle.Success) ); + + // save configuration + settings.ticket.staff_roles = staffRoles; + await settings.save(); + await targetChannel.send({ embeds: [embed], components: [tktBtnRow] }); await modal.deleteReply(); await sentMsg.edit({ content: "Done! Ticket Message Created", components: [] }); diff --git a/src/commands/utility/covid.js b/src/commands/utility/covid.js index ca3edff02..1a4f48d9a 100644 --- a/src/commands/utility/covid.js +++ b/src/commands/utility/covid.js @@ -43,7 +43,7 @@ module.exports = { }; async function getCovid(country) { - const response = await getJson(`https://disease.sh/v2/countries/${country}`); + const response = await getJson(`https://corona.lmao.ninja/v2/countries/${country}`); if (response.status === 404) return "```css\nCountry with the provided name is not found```"; if (!response.success) return MESSAGES.API_ERROR; diff --git a/src/database/schemas/Guild.js b/src/database/schemas/Guild.js index 25e2b2f76..292ec0328 100644 --- a/src/database/schemas/Guild.js +++ b/src/database/schemas/Guild.js @@ -33,6 +33,8 @@ const Schema = new mongoose.Schema({ staff_roles: [String], }, ], + staff_roles: [String], // Add this line + category_channel: String, }, automod: { debug: Boolean, @@ -117,8 +119,9 @@ module.exports = { * @param {import('discord.js').Guild} guild */ getSettings: async (guild) => { - if (!guild) throw new Error("Guild is undefined"); - if (!guild.id) throw new Error("Guild Id is undefined"); + if (!guild || !guild.id) { + throw new Error("Guild or Guild Id is undefined"); + } const cached = cache.get(guild.id); if (cached) return cached; @@ -151,3 +154,4 @@ module.exports = { return guildData; }, }; + diff --git a/src/handlers/ticket.js b/src/handlers/ticket.js index 175a53c39..909b5b602 100644 --- a/src/handlers/ticket.js +++ b/src/handlers/ticket.js @@ -18,7 +18,6 @@ const { error } = require("@helpers/Logger"); const OPEN_PERMS = ["ManageChannels"]; const CLOSE_PERMS = ["ManageChannels", "ReadMessageHistory"]; - /** * @param {import('discord.js').Channel} channel */ @@ -76,7 +75,7 @@ async function closeTicket(channel, closedBy, reason) { let content = ""; reversed.forEach((m) => { - content += `[${new Date(m.createdAt).toLocaleString("en-US")}] - ${m.author.username}\n`; + content += `[${new Date(m.createdAt).toLocaleString("en-US")}] - ${m.author.tag}\n`; if (m.cleanContent !== "") content += `${m.cleanContent}\n`; if (m.attachments.size > 0) content += `${m.attachments.map((att) => att.proxyURL).join(", ")}\n`; content += "\n"; @@ -96,19 +95,19 @@ async function closeTicket(channel, closedBy, reason) { if (channel.deletable) await channel.delete(); - const embed = new EmbedBuilder().setAuthor({ name: "Ticket Closed" }).setColor(TICKET.CLOSE_EMBED); + const embed = new EmbedBuilder().setAuthor({ name: "Ticket Closed & deleted" }).setColor(TICKET.CLOSE_EMBED); const fields = []; if (reason) fields.push({ name: "Reason", value: reason, inline: false }); fields.push( { name: "Opened By", - value: ticketDetails.user ? ticketDetails.user.username : "Unknown", + value: ticketDetails.user ? ticketDetails.user.tag : "Unknown", inline: true, }, { name: "Closed By", - value: closedBy ? closedBy.username : "Unknown", + value: closedBy ? closedBy.tag : "Unknown", inline: true, } ); @@ -170,7 +169,16 @@ async function handleTicketOpen(interaction) { if (alreadyExists) return interaction.followUp(`You already have an open ticket`); const settings = await getSettings(guild); + // Retrieve the category ID from guild settings + const categoryId = settings.ticket.category_channel; + + // Get the category channel by ID + const categoryChannel = guild.channels.cache.get(categoryId); + // Ensure that the category channel exists and is a category + if (!categoryChannel || categoryChannel.type !== 4) { + return interaction.followUp("Invalid category ID set for ticket creation."); + } // limit check const existing = getTicketChannels(guild).size; if (existing > settings.ticket.limit) return interaction.followUp("There are too many open tickets. Try again later"); @@ -204,23 +212,41 @@ async function handleTicketOpen(interaction) { catName = res.values[0]; catPerms = categories.find((cat) => cat.name === catName)?.staff_roles || []; } - + // Retrieve category channel ID from guild settings + let catChannel = null; + if (categoryId) { + catChannel = guild.channels.cache.get(categoryId); + } try { const ticketNumber = (existing + 1).toString(); - const permissionOverwrites = [ - { - id: guild.roles.everyone, - deny: ["ViewChannel"], - }, - { - id: user.id, - allow: ["ViewChannel", "SendMessages", "ReadMessageHistory"], - }, - { - id: guild.members.me.roles.highest.id, - allow: ["ViewChannel", "SendMessages", "ReadMessageHistory"], - }, - ]; +const permissionOverwrites = [ + { + id: guild.roles.everyone.id, + deny: ["ViewChannel"], + }, + { + id: user.id, + allow: ["ViewChannel", "SendMessages", "ReadMessageHistory"], + }, +]; + +// Get staff roles from settings +const staffRoles = settings.ticket?.staff_roles || []; + + +// Loop through each staff role ID +staffRoles.forEach(roleId => { + const role = guild.roles.cache.get(roleId); + if (role) { + permissionOverwrites.push({ + id: role.id, + allow: ["ViewChannel", "SendMessages", "ReadMessageHistory"], + }); + } +}); + + + if (catPerms?.length > 0) { catPerms?.forEach((roleId) => { @@ -233,18 +259,22 @@ async function handleTicketOpen(interaction) { }); } + const username = interaction.user.username; + const tktChannel = await guild.channels.create({ - name: `tіcket-${ticketNumber}`, + name: `${username}-${ticketNumber}`, type: ChannelType.GuildText, - topic: `tіcket|${user.id}|${catName || "Default"}`, + topic: `${username}|${user.id}|${catName || "Default"}`, permissionOverwrites, + parent: categoryId, }); +const staffRolesPing = staffRoles.map(roleId => `<@&${roleId}>`).join(' '); const embed = new EmbedBuilder() .setAuthor({ name: `Ticket #${ticketNumber}` }) .setDescription( `Hello ${user.toString()} - Support will be with you shortly + ${staffRolesPing} will be with you shortly ${catName ? `\n**Category:** ${catName}` : ""} ` ) @@ -252,13 +282,20 @@ async function handleTicketOpen(interaction) { let buttonsRow = new ActionRowBuilder().addComponents( new ButtonBuilder() - .setLabel("Close Ticket") + .setLabel("Close & delete Ticket") .setCustomId("TICKET_CLOSE") .setEmoji("🔒") .setStyle(ButtonStyle.Primary) ); - - const sent = await tktChannel.send({ content: user.toString(), embeds: [embed], components: [buttonsRow] }); + // Ping staff roles if present + if (staffRoles.length > 0) { + const staffRolesPing = staffRoles.map(roleId => `<@&${roleId}>`).join(' '); + const messageContent = `**New ticket**\n${staffRolesPing}`; + await tktChannel.send({ content: messageContent, allowedMentions: { parse: ["everyone", "roles", "users"] } }); + } else { + await tktChannel.send("**New ticket**"); + } + const sent = await tktChannel.send({ content: user.toString(), embeds: [embed], components: [buttonsRow], allowedMentions: { parse: ["users"] } }); const dmEmbed = new EmbedBuilder() .setColor(TICKET.CREATE_EMBED) @@ -276,7 +313,7 @@ async function handleTicketOpen(interaction) { user.send({ embeds: [dmEmbed], components: [row] }).catch((ex) => {}); - await interaction.editReply(`Ticket created! 🔥`); + await interaction.editReply(`💯 Ticket created! in\n${tktChannel}`); } catch (ex) { error("handleTicketOpen", ex); return interaction.editReply("Failed to create ticket channel, an error occurred!");