From 053d7a745825b4e2e461f82404a0fdaa98a7ba9d Mon Sep 17 00:00:00 2001 From: William Harrison Date: Sat, 30 Sep 2023 13:12:47 +0800 Subject: [PATCH] feat: testing channels + better delete logs + more --- src/commands/bot-admin/testing.ts | 299 ++++++++++++++++++++++++++++++ src/commands/dev/poll-ping.ts | 31 +++- src/config.ts | 15 +- src/events/guild/messageCreate.ts | 7 +- src/events/guild/messageDelete.ts | 30 ++- src/functions/roles/get.ts | 36 ++-- src/index.ts | 7 +- src/models/TestingChannel.ts | 9 + src/util/testing-channels.ts | 24 +++ 9 files changed, 406 insertions(+), 52 deletions(-) create mode 100644 src/commands/bot-admin/testing.ts create mode 100644 src/models/TestingChannel.ts create mode 100644 src/util/testing-channels.ts diff --git a/src/commands/bot-admin/testing.ts b/src/commands/bot-admin/testing.ts new file mode 100644 index 0000000..97def2c --- /dev/null +++ b/src/commands/bot-admin/testing.ts @@ -0,0 +1,299 @@ +import Command from "../../classes/Command"; +import ExtendedClient from "../../classes/ExtendedClient"; +import { CommandInteraction, TextChannel } from "discord.js"; + +import { emojis as emoji } from "../../config"; +import { randomUUID } from "crypto"; + +import TestingChannel from "../../models/TestingChannel"; + +const command: Command = { + name: "testing", + description: "Create and manage testing channels.", + options: [ + { + type: 1, + name: "create", + description: "Create a testing channel." + }, + + { + type: 1, + name: "delete", + description: "Delete a testing channel. Can only be used in a testing channel." + }, + + { + type: 2, + name: "user", + description: "Add or remove users from your testing channel.", + options: [ + { + type: 1, + name: "add", + description: "Add a user to your testing channel.", + options: [ + { + type: 6, + name: "user", + description: "The user to add.", + required: true + } + ] + }, + + { + type: 1, + name: "remove", + description: "Remove a user from your testing channel.", + options: [ + { + type: 6, + name: "user", + description: "The user to remove.", + required: true + } + ] + } + ] + } + ], + default_member_permissions: null, + botPermissions: ["ManageChannels"], + requiredRoles: ["botAdmin"], + cooldown: 10, + enabled: true, + deferReply: true, + ephemeral: true, + async execute(interaction: CommandInteraction & any, client: ExtendedClient, Discord: typeof import("discord.js")) { + try { + if(interaction.options.getSubcommand() === "create") { + const creating = new Discord.EmbedBuilder() + .setColor(client.config_embeds.default) + .setDescription(`${emoji.ping} Creating a testing channel...`) + + await interaction.editReply({ embeds: [creating] }); + + const channel = await interaction.guild.channels.create({ + name: `testing-${randomUUID().slice(0, 8)}`, + type: Discord.ChannelType.GuildText, + permissionOverwrites: [ + { + id: interaction.guild.id, + deny: ["ViewChannel"], + allow: ["EmbedLinks", "ReadMessageHistory", "SendMessages", "UseExternalEmojis"] + }, + + { + id: client.user.id, + allow: ["ViewChannel", "ManageChannels", "ManageMessages"] + }, + + { + id: interaction.user.id, + allow: ["ViewChannel"] + } + ] + }) + + const data = await new TestingChannel({ + channel: channel.id, + created: Date.now(), + owner: interaction.user.id + }).save() + + const welcome = new Discord.EmbedBuilder() + .setColor(client.config_embeds.default) + .setTitle("Your Testing Channel") + .setDescription(`Welcome to your testing channel, **${interaction.user.globalName || interaction.user.username}**!\n\nThis channel has been setup with overrides so only you and server admins can access it.\n\n**This channel will be automatically deleted after 24 hours.**`) + .addFields ( + { name: "Your Permissions", value: "```yaml\n- Embed Links\n- Read Message History\n- Send Messages\n- Use External Emojis\n- View Channel\n```" }, + { name: "Delete Channel", value: `To delete this channel, use the command: \`/testing delete\`` } + ) + .setTimestamp() + + const channelInfo = new Discord.EmbedBuilder() + .setColor(client.config_embeds.default) + .setTitle("Channel Information") + .addFields ( + { name: "Owner", value: `${interaction.user}` }, + { name: "Created", value: ` ()` }, + { name: "Expires", value: ` ()` } + ) + .setTimestamp() + + const welcomeMsg = await channel.send({ content: `${interaction.user}`, embeds: [welcome, channelInfo] }); + + await welcomeMsg.pin(); + + const created = new Discord.EmbedBuilder() + .setColor(client.config_embeds.default) + .setDescription(`${emoji.tick} Created testing channel: ${channel}`) + + await interaction.editReply({ embeds: [created] }); + return; + } + + if(interaction.options.getSubcommand() === "delete") { + const channel = interaction.channel as TextChannel; + + if(!channel.name.startsWith("testing-")) { + const error = new Discord.EmbedBuilder() + .setColor(client.config_embeds.error) + .setDescription(`${emoji.cross} This command can only be used in a testing channel.`) + + await interaction.editReply({ embeds: [error] }); + return; + } + + const data = await TestingChannel.findOne({ channel: channel.id }); + + if(interaction.user.id !== data.owner) { + const error = new Discord.EmbedBuilder() + .setColor(client.config_embeds.error) + .setDescription(`${emoji.cross} You do not own this testing channel!`) + + await interaction.editReply({ embeds: [error] }); + return; + } + + const deleting = new Discord.EmbedBuilder() + .setColor(client.config_embeds.default) + .setDescription(`${emoji.ping} Deleting testing channel...`) + + await interaction.editReply({ embeds: [deleting] }); + + await channel.delete(); + await data.delete(); + return; + } + + if(interaction.options.getSubcommandGroup() === "user") { + const user = interaction.options.getUser("user"); + const channel = interaction.channel as TextChannel; + + if(interaction.options.getSubcommand() === "add") { + if(!channel.name.startsWith("testing-")) { + const error = new Discord.EmbedBuilder() + .setColor(client.config_embeds.error) + .setDescription(`${emoji.cross} This command can only be used in a testing channel.`) + + await interaction.editReply({ embeds: [error] }); + return; + } + + const data = await TestingChannel.findOne({ channel: channel.id }); + + if(interaction.user.id !== data.owner) { + const error = new Discord.EmbedBuilder() + .setColor(client.config_embeds.error) + .setDescription(`${emoji.cross} You do not own this testing channel!`) + + await interaction.editReply({ embeds: [error] }); + return; + } + + const member = await interaction.guild.members.fetch(user.id); + + if(!member) { + const error = new Discord.EmbedBuilder() + .setColor(client.config_embeds.error) + .setDescription(`${emoji.cross} ${user} is not in this server!`) + + await interaction.editReply({ embeds: [error] }); + return; + } + + const perms = channel.permissionOverwrites.cache.get(member.id); + + if(perms) { + const error = new Discord.EmbedBuilder() + .setColor(client.config_embeds.error) + .setDescription(`${emoji.cross} ${user} is already in this testing channel!`) + + await interaction.editReply({ embeds: [error] }); + return; + } + + await channel.permissionOverwrites.create(member.id, { ViewChannel: true }); + + const welcome = new Discord.EmbedBuilder() + .setColor(client.config_embeds.default) + .setDescription(`${user} has been added to the testing channel.`) + + interaction.channel.send({ embeds: [welcome] }); + + const added = new Discord.EmbedBuilder() + .setColor(client.config_embeds.default) + .setDescription(`${emoji.tick} Added ${member} to the testing channel.`) + + await interaction.editReply({ embeds: [added] }); + return; + } + + if(interaction.options.getSubcommand() === "remove") { + if(!channel.name.startsWith("testing-")) { + const error = new Discord.EmbedBuilder() + .setColor(client.config_embeds.error) + .setDescription(`${emoji.cross} This command can only be used in a testing channel.`) + + await interaction.editReply({ embeds: [error] }); + return; + } + + const data = await TestingChannel.findOne({ channel: channel.id }); + + if(interaction.user.id !== data.owner) { + const error = new Discord.EmbedBuilder() + .setColor(client.config_embeds.error) + .setDescription(`${emoji.cross} You do not own this testing channel!`) + + await interaction.editReply({ embeds: [error] }); + return; + } + + const member = await interaction.guild.members.fetch(user.id); + + if(!member) { + const error = new Discord.EmbedBuilder() + .setColor(client.config_embeds.error) + .setDescription(`${emoji.cross} ${user} is not in this server!`) + + await interaction.editReply({ embeds: [error] }); + return; + } + + const perms = channel.permissionOverwrites.cache.get(member.id); + + if(!perms) { + const error = new Discord.EmbedBuilder() + .setColor(client.config_embeds.error) + .setDescription(`${emoji.cross} ${user} is not in this testing channel!`) + + await interaction.editReply({ embeds: [error] }); + return; + } + + await perms.delete(); + + const goodbye = new Discord.EmbedBuilder() + .setColor(client.config_embeds.default) + .setDescription(`${user} has been removed from the testing channel.`) + + interaction.channel.send({ embeds: [goodbye] }); + + const removed = new Discord.EmbedBuilder() + .setColor(client.config_embeds.default) + .setDescription(`${emoji.tick} Removed ${member} from the testing channel.`) + + await interaction.editReply({ embeds: [removed] }); + return; + } + } + } catch(err) { + client.logCommandError(err, interaction, Discord); + } + } +} + +export = command; diff --git a/src/commands/dev/poll-ping.ts b/src/commands/dev/poll-ping.ts index b22f6aa..f35bafb 100644 --- a/src/commands/dev/poll-ping.ts +++ b/src/commands/dev/poll-ping.ts @@ -21,13 +21,25 @@ const command: Command = { requiredRoles: [], cooldown: 0, enabled: true, - deferReply: false, - ephemeral: false, + deferReply: true, + ephemeral: true, async execute(interaction: CommandInteraction, client: ExtendedClient, Discord: typeof import("discord.js")) { try { - // Return error if the user is not allowed to use the command - if(!client.config_main.pollPingAllowed.includes(interaction.user.id)) { - await interaction.reply({ embeds: [noPermissionCommand], ephemeral: true }); + // Return error if the command is not in the primary guild + if(interaction.guild.id !== client.config_main.primaryGuild) { + const error = new Discord.EmbedBuilder() + .setColor(client.config_embeds.error) + .setDescription(`${emoji.cross} This command can only be used in the primary guild.`) + + await interaction.editReply({ embeds: [error] }); + return; + } + + // Return error if the user does not have the pollPingAllowed role + const roles = await interaction.guild.members.fetch(interaction.user.id).then(member => member.roles.cache.map(role => role.id)); + + if(!roles.includes(client.config_roles.pollPingAllowed)) { + await interaction.editReply({ embeds: [noPermissionCommand] }); return; } @@ -37,7 +49,7 @@ const command: Command = { .setColor(client.config_embeds.error) .setDescription(`${emoji.cross} This command can only be used in <#${client.config_channels.devQuestions}>.`) - await interaction.reply({ embeds: [error], ephemeral: true }); + await interaction.editReply({ embeds: [error] }); return; } @@ -48,7 +60,7 @@ const command: Command = { .setColor(client.config_embeds.error) .setDescription(`${emoji.cross} This command is on cooldown, it can be used .`) - await interaction.reply({ embeds: [error], ephemeral: true }); + await interaction.editReply({ embeds: [error] }); return; } @@ -58,12 +70,13 @@ const command: Command = { client.lastPoll = Date.now(); // Ping the role - await interaction.reply({ content: `<@&${client.config_roles.pollPing}>` }); + await interaction.channel.send({ content: `testing` }); + await interaction.deleteReply(); // Log the command const log = new Discord.EmbedBuilder() .setColor(client.config_embeds.default) - .setTitle("Poll Ping Command Used") + .setTitle("Poll Ping") .addFields ( { name: "User", value: `${interaction.user}` }, { name: "Reason", value: reason } diff --git a/src/config.ts b/src/config.ts index 0710d72..9e787cc 100644 --- a/src/config.ts +++ b/src/config.ts @@ -33,21 +33,15 @@ const emojis = { const main = { appealEmail: "dan@danbot.host", - disablePermCheck: ["853158265466257448"] as Snowflake[], // Role check bypass + disablePermCheck: [ + "853158265466257448" // William + ] as Snowflake[], dmAllowed: [ "137624084572798976", // Dan "757296951925538856", // DIBSTER "853158265466257448" // William ], legacyPrefix: "DBHB!" as string, - pollPingAllowed: [ - "137624084572798976", // Dan - "405771597761216522", // Mike - "599204289088585738", // Dotto - "757296951925538856", // DIBSTER - "712560683216011274", // FC - "853158265466257448" // William - ], primaryGuild: "639477525927690240" as Snowflake, // DanBot Hosting } @@ -60,6 +54,7 @@ const roles = { mod: "898041748817842176", owner: "898041741695926282", pollPing: "898041781927682090", + pollPingAllowed: "1157497265708093520", staff: "898041751099539497" } @@ -74,7 +69,7 @@ const starboard = { "898354771927400538" // #beta-lounge ], emoji: "⭐", // The emoji to react with - threshold: 5 // Minimum reactions required to post on starboard + threshold: 3 // Minimum reactions required to post on starboard } export { diff --git a/src/events/guild/messageCreate.ts b/src/events/guild/messageCreate.ts index 57a5db9..5f9c52e 100644 --- a/src/events/guild/messageCreate.ts +++ b/src/events/guild/messageCreate.ts @@ -27,7 +27,7 @@ const event: Event = { if(message.channel.type === ChannelType.DM && main.dmAllowed.includes(message.author.id)) { const args = message.content.trim().split(/ +/g); - if(!args[1]) return message.reply("Please provide the text you would like to send to the channel."); + if(!args[1]) return message.reply("Please provide the message you would like to send."); try { const channel = client.channels.cache.get(args[0]) as TextChannel; @@ -102,10 +102,9 @@ const event: Event = { if(!command) { // Prefix command deprecation const description = [ - `👋 Hey there ${message.author}!`, - `\nIn the recent rewrite of the DBH Discord bot we have decided to move away from prefix commands (e.g. \`${main.legacyPrefix}help\`) and have moved to slash commands (e.g. \`/help\`).`, + `👋 Hey there, **${message.author.globalName || message.author.username}**!`, + `\nIn the recent rewrite of the DBH Discord bot we have decided to move away from prefix commands (e.g. \`${main.legacyPrefix}help\`) and have moved to slash commands (e.g. ).`, "\nThis change has been made to help make development easier of the Discord bot and allow us to maintain it easily.", - `\nTry out one of the new slash commands: `, "\nRegards,", "The **DanBot Team**" ] diff --git a/src/events/guild/messageDelete.ts b/src/events/guild/messageDelete.ts index db7a53a..cb40652 100644 --- a/src/events/guild/messageDelete.ts +++ b/src/events/guild/messageDelete.ts @@ -15,39 +15,35 @@ const event: Event = { // Ignore messages not in the primary guild // Also ignore partial messages and messages that are only embeds - if(message.partial || (message.embeds.length && !message.content && !message.attachments.size) || !message.guild) return; + if(!message.guild || !message.content && !message.attachments.size) return; if(message.guild.id !== main.primaryGuild) return; // Ignore messages if the bot does not have the required permissions if(!message.guild.members.me.permissions.has(requiredPerms)) return; + if(message.partial) await message.fetch(); + const channel = message.guild.channels.cache.get(channels.messageLogs) as TextChannel; const log = new Discord.EmbedBuilder() .setColor(client.config_embeds.default) .setAuthor({ name: message.author.tag.endsWith("#0") ? message.author.username : message.author.tag, iconURL: message.author.displayAvatarURL({ extension: "png", forceStatic: false }), url: `https://discord.com/users/${message.author.id}` }) .setTitle("Message Deleted") - .setDescription(cap(message.content, 2000) ?? "*Message contained no content.*") + .setDescription(cap(`${message.content || "*No message content.*"}`, 2000)) .addFields ( + { name: "Message ID", value: `\`${message.id}\``, inline: true }, + { name: "Message Sent", value: ``, inline: true }, + { name: "Attachments", value: `${message.attachments.size}`, inline: true }, { name: "Channel", value: `<#${message.channel.id}>`, inline: true } ) .setTimestamp() - // If the message had an attachment, add it to the embed - let attachment = null; - - if(message.attachments.first()) { - const fileExt = path.extname(message.attachments.first().url.toLowerCase()); - const allowedExtensions = ["gif", "jpeg", "jpg", "png", "svg", "webp"]; - - if(allowedExtensions.includes(fileExt.split(".").join(""))) { - attachment = new Discord.AttachmentBuilder(message.attachments.first().url, { name: `attachment${fileExt}` }); - - log.setImage(`attachment://${attachment.name}`); - } + if(!message.attachments.size) { + channel.send({ embeds: [log] }); + } else { + const attachments = message.attachments.map(attachment => attachment); + channel.send({ embeds: [log], files: attachments }); } - channel.send({ embeds: [log] }); - // Delete starboard message if it exists // Return if the message is one week old if(message.createdTimestamp < Date.now() - 604800000) return; @@ -64,7 +60,7 @@ const event: Event = { const starboardChannel = message.guild.channels.cache.get(channels.starboard) as TextChannel; const messages = await starboardChannel.messages.fetch({ limit: 100 }); - const starMessage = messages.find(msg => msg.author.id === client.user.id && msg.embeds.length === 1 && msg.embeds[0].footer.text === `ID: ${message.id}`); + const starMessage = messages.find(msg => msg.author.id === client.user.id && msg?.embeds?.length === 1 && msg.embeds[0]?.footer?.text === `ID: ${message.id}`); if(starMessage) starMessage.delete(); } catch(err) { diff --git a/src/functions/roles/get.ts b/src/functions/roles/get.ts index b62bc7e..70e3be0 100644 --- a/src/functions/roles/get.ts +++ b/src/functions/roles/get.ts @@ -6,17 +6,31 @@ import { roles as role } from "../../config"; import Roles from "../../classes/Roles"; export default async function (userId: Snowflake, client: ExtendedClient & any): Promise { - // Fetch user roles - const roles = client.guilds.cache.get(client.config_main.primaryGuild).members.cache.get(userId).roles.cache.map((role: Role) => role.id) || []; + try { + // Fetch user roles + const guild = await client.guilds.fetch(client.config_main.primaryGuild); + const roles = guild.members.cache.get(userId).roles.cache.map((role: Role) => role.id) || []; - return { - owner: roles.includes(role.owner), - botAdmin: roles.includes(role.botAdmin), - admin: roles.includes(role.admin), - dev: roles.includes(role.dev), - mod: roles.includes(role.mod), - helper: roles.includes(role.helper), - staff: roles.includes(role.staff), - donator: roles.includes(role.donator) + return { + owner: roles.includes(role.owner), + botAdmin: roles.includes(role.botAdmin), + admin: roles.includes(role.admin), + dev: roles.includes(role.dev), + mod: roles.includes(role.mod), + helper: roles.includes(role.helper), + staff: roles.includes(role.staff), + donator: roles.includes(role.donator) + } + } catch(err) { + return { + owner: false, + botAdmin: false, + admin: false, + dev: false, + mod: false, + helper: false, + staff: false, + donator: false + } } } diff --git a/src/index.ts b/src/index.ts index e197bfa..a2bf5d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -55,11 +55,16 @@ client.legacyCommands = new Discord.Collection(); import { loadHandlers } from "./util/functions"; loadHandlers(client); -// Check and update server status every 30 seconds +// Check and update server status every 60 seconds import checker from "./util/server-status/checker"; checker(client); setInterval(() => checker(client), 60000); +// Check and update testing channel data every 5 minutes +import testingChannels from "./util/testing-channels"; +testingChannels(client); +setInterval(() => testingChannels(client), 300000); + // Login client.login(process.env.token); diff --git a/src/models/TestingChannel.ts b/src/models/TestingChannel.ts new file mode 100644 index 0000000..316a123 --- /dev/null +++ b/src/models/TestingChannel.ts @@ -0,0 +1,9 @@ +import { model, Schema } from "mongoose"; + +const schema = new Schema({ + channel: String, + created: Number, + owner: String +}) + +export default model("testing-channels", schema, "testing-channels"); diff --git a/src/util/testing-channels.ts b/src/util/testing-channels.ts new file mode 100644 index 0000000..abdae06 --- /dev/null +++ b/src/util/testing-channels.ts @@ -0,0 +1,24 @@ +import ExtendedClient from "../classes/ExtendedClient"; + +import TestingChannel from "../models/TestingChannel"; + +import { main } from "../config"; + +export default async function (client: ExtendedClient) { + const data = await TestingChannel.find({}); + + const guild = client.guilds.cache.get(main.primaryGuild); + + for(const item of data) { + if(!guild.channels.cache.get(item.channel)) await item.delete(); + + // Delete channels after 24 hours + if(Date.now() - item.created >= 86400000) { + const channel = guild.channels.cache.get(item.channel) as import("discord.js").TextChannel; + + if(channel) await channel.delete(); + + await item.delete(); + } + } +}