From e6ba78866258405596508b7772feb374a3af79c8 Mon Sep 17 00:00:00 2001 From: PineaFan Date: Wed, 18 Jan 2023 20:41:16 +0000 Subject: [PATCH] All commands and some events finished --- src/actions/createModActionTicket.ts | 200 ++++++++++++++++++--------- src/commands/mod/nick.ts | 156 ++++++++++++--------- src/commands/mod/warn.ts | 2 +- src/events/channelUpdate.ts | 99 ++++++------- src/events/emojiCreate.ts | 17 ++- src/events/emojiDelete.ts | 20 +-- src/events/emojiUpdate.ts | 27 ++-- src/events/guildBanAdd.ts | 20 +-- src/events/guildBanRemove.ts | 18 +-- src/premium/attachmentLogs.ts | 5 +- src/reflex/guide.ts | 87 ++++-------- src/reflex/statsChannelUpdate.ts | 3 +- src/reflex/welcome.ts | 5 +- src/utils/database.ts | 4 + src/utils/log.ts | 2 +- 15 files changed, 376 insertions(+), 289 deletions(-) diff --git a/src/actions/createModActionTicket.ts b/src/actions/createModActionTicket.ts index 481666b..d6e9cd9 100644 --- a/src/actions/createModActionTicket.ts +++ b/src/actions/createModActionTicket.ts @@ -6,7 +6,7 @@ import client from "../utils/client.js"; export async function create( guild: Discord.Guild, - member: Discord.User, + user: Discord.User, createdBy: Discord.User, reason: string | null, customReason?: string @@ -14,7 +14,7 @@ export async function create( const config = await client.database.guilds.read(guild.id); const { log, NucleusColors, entry, renderUser, renderChannel, renderDelta } = client.logger; const overwrites = [{ - id: member, + id: user, allow: ["ViewChannel", "SendMessages", "AttachFiles", "AddReactions", "ReadMessageHistory"], type: OverwriteType.Member }] as unknown as Discord.OverwriteResolvable[]; @@ -30,78 +30,146 @@ export async function create( type: OverwriteType.Role }); } + const targetChannel: Discord.CategoryChannel | Discord.TextChannel = (await guild.channels.fetch(config.tickets.category!))! as Discord.CategoryChannel | Discord.TextChannel; - let c; - try { - c = await guild.channels.create({ - name: member.username, - type: ChannelType.GuildText, - topic: `${member.id} Active`, - parent: config.tickets.category, - nsfw: false, - permissionOverwrites: overwrites as Discord.OverwriteResolvable[], - reason: "Creating ticket" - }); - } catch (e) { - return null; - } - try { - await c.send({ - content: - `<@${member.id}>` + (config.tickets.supportRole !== null ? ` • <@&${config.tickets.supportRole}>` : ""), - allowedMentions: { - users: [member.id], - roles: config.tickets.supportRole !== null ? [config.tickets.supportRole] : [] + let c: Discord.TextChannel | Discord.PrivateThreadChannel; + if (targetChannel.type === Discord.ChannelType.GuildCategory) { + const overwrites = [ + { + id: user, + allow: ["ViewChannel", "SendMessages", "AttachFiles", "AddReactions", "ReadMessageHistory"], + type: Discord.OverwriteType.Member } + ] as Discord.OverwriteResolvable[]; + overwrites.push({ + id: guild.roles.everyone, + deny: ["ViewChannel"], + type: Discord.OverwriteType.Role }); - await c.send({ - embeds: [ - new EmojiEmbed() - .setTitle("New Ticket") - .setDescription( - "Ticket created by a Moderator\n" + + if (config.tickets.supportRole !== null) { + overwrites.push({ + id: guild.roles.cache.get(config.tickets.supportRole)!, + allow: ["ViewChannel", "SendMessages", "AttachFiles", "AddReactions", "ReadMessageHistory"], + type: Discord.OverwriteType.Role + }); + } + + try { + c = await guild.channels.create({ + name: `${user.username.toLowerCase()}`, + type: ChannelType.GuildText, + topic: `${user.id} Active`, + parent: config.tickets.category, + nsfw: false, + permissionOverwrites: overwrites as Discord.OverwriteResolvable[], + reason: "Creating ticket" + }); + } catch (e) { + return null; + } + try { + await c.send({ + content: + `<@${user.id}>` + + (config.tickets.supportRole !== null ? ` • <@&${config.tickets.supportRole}>` : ""), + allowedMentions: { + users: [user.id], + roles: config.tickets.supportRole !== null ? [config.tickets.supportRole] : [] + } + }); + await c.send({ + embeds: [ + new EmojiEmbed() + .setTitle("New Ticket") + .setDescription( + "Ticket created by a Moderator\n" + `**Support type:** ${customReason ? customReason : "Appeal submission"}\n` + (reason !== null ? `**Reason:**\n> ${reason}\n` : "") + `**Ticket ID:** \`${c.id}\`\n` + `Type ${await getCommandMentionByName("ticket/close")} to close this ticket.` - ) - .setStatus("Success") - .setEmoji("GUILD.TICKET.OPEN") - ], - components: [ - new ActionRowBuilder().addComponents([ - new ButtonBuilder() - .setLabel("Close") - .setStyle(ButtonStyle.Danger) - .setCustomId("closeticket") - .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) - ]) - ] - }); - const data = { - meta: { - type: "ticketCreate", - displayName: "Ticket Created", - calculateType: "ticketUpdate", - color: NucleusColors.green, - emoji: "GUILD.TICKET.OPEN", - timestamp: new Date().getTime() - }, - list: { - ticketFor: entry(member.id, renderUser(member)), - createdBy: entry(createdBy.id, renderUser(createdBy)), - created: entry((new Date().getTime()).toString(), renderDelta(new Date().getTime())), - ticketChannel: entry(c.id, renderChannel(c)) - }, - hidden: { - guild: guild.id - } - }; - log(data); - } catch (e) { - console.log(e); - return null; + ) + .setStatus("Success") + .setEmoji("GUILD.TICKET.OPEN") + ], + components: [ + new ActionRowBuilder().addComponents([ + new ButtonBuilder() + .setLabel("Close") + .setStyle(ButtonStyle.Danger) + .setCustomId("closeticket") + .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) + ]) + ] + }); + } catch (e) { + return null; + } + } else { + c = await targetChannel.threads.create({name: `${user.username} - ${user.id} - Active`, + autoArchiveDuration: 60 * 24 * 7, + type: Discord.ChannelType.PrivateThread, + reason: "Creating ticket" + }) as Discord.PrivateThreadChannel; + c.members.add(user.id); + c.members.add(createdBy.id); + try { + await c.send({ + content: + `<@${user.id}>` + + (config.tickets.supportRole !== null ? ` • <@&${config.tickets.supportRole}>` : ""), + allowedMentions: { + users: [user.id], + roles: config.tickets.supportRole !== null ? [config.tickets.supportRole] : [] + } + }); + await c.send({ + embeds: [ + new EmojiEmbed() + .setTitle("New Ticket") + .setDescription( + "Ticket created by a Moderator\n" + + `**Support type:** ${customReason ? customReason : "Appeal submission"}\n` + + (reason !== null ? `**Reason:**\n> ${reason}\n` : "") + + `**Ticket ID:** \`${c.id}\`\n` + + `Type ${await getCommandMentionByName("ticket/close")} to close this ticket.` + ) + .setStatus("Success") + .setEmoji("GUILD.TICKET.OPEN") + ], + components: [ + new ActionRowBuilder().addComponents([ + new ButtonBuilder() + .setLabel("Close") + .setStyle(ButtonStyle.Danger) + .setCustomId("closeticket") + .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) + ]) + ] + }); + } catch (e) { + return null; + } } + const data = { + meta: { + type: "ticketCreate", + displayName: "Ticket Created", + calculateType: "ticketUpdate", + color: NucleusColors.green, + emoji: "GUILD.TICKET.OPEN", + timestamp: new Date().getTime() + }, + list: { + ticketFor: entry(user.id, renderUser(user)), + createdBy: entry(createdBy.id, renderUser(createdBy)), + created: entry((new Date().getTime()).toString(), renderDelta(new Date().getTime())), + ticketChannel: entry(c.id, renderChannel(c)) + }, + hidden: { + guild: guild.id + } + }; + log(data); return c.id; } diff --git a/src/commands/mod/nick.ts b/src/commands/mod/nick.ts index 1fbde64..abb695d 100644 --- a/src/commands/mod/nick.ts +++ b/src/commands/mod/nick.ts @@ -1,9 +1,11 @@ -import type { CommandInteraction, GuildMember } from "discord.js"; +import { LinkWarningFooter } from './../../utils/defaults.js'; +import { ActionRowBuilder, ButtonBuilder, CommandInteraction, GuildMember, ButtonStyle, Message } from "discord.js"; import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; import confirmationMessage from "../../utils/confirmationMessage.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import keyValueList from "../../utils/generateKeyValueList.js"; import client from "../../utils/client.js"; +import { areTicketsEnabled, create } from "../../actions/createModActionTicket.js"; const command = (builder: SlashCommandSubcommandBuilder) => builder @@ -17,123 +19,122 @@ const command = (builder: SlashCommandSubcommandBuilder) => builder const callback = async (interaction: CommandInteraction): Promise => { - const { renderUser } = client.logger; + const { log, NucleusColors, entry, renderDelta, renderUser } = client.logger; // TODO:[Modals] Replace this with a modal let notify = true; let confirmation; let timedOut = false; let success = false; - while (!timedOut && !success) { + let createAppealTicket = false; + let firstRun = true; + do { confirmation = await new confirmationMessage(interaction) .setEmoji("PUNISH.NICKNAME.RED") .setTitle("Nickname") .setDescription( keyValueList({ - user: renderUser(interaction.options.getUser("user")), + user: renderUser(interaction.options.getUser("user")!), "new nickname": `${ - interaction.options.getString("name") ? interaction.options.getString("name") : "*No nickname*" + interaction.options.get("name")?.value as string ? interaction.options.get("name")?.value as string : "*No nickname*" }` }) + - `The user **will${notify ? "" : " not"}** be notified\n\n` + - `Are you sure you want to ${interaction.options.getString("name") ? "change" : "clear"} <@!${ + `Are you sure you want to ${interaction.options.get("name")?.value as string ? "change" : "clear"} <@!${ (interaction.options.getMember("user") as GuildMember).id }>'s nickname?` ) .setColor("Danger") + .addCustomBoolean( + "appeal", + "Create appeal ticket", + !(await areTicketsEnabled(interaction.guild!.id)), + async () => await create(interaction.guild!, interaction.options.getUser("user")!, interaction.user, "Nickname changed"), + "An appeal ticket will be created", + null, + "CONTROL.TICKET", + createAppealTicket + ) .addCustomBoolean( "notify", "Notify user", false, null, - null, + "The user will be sent a DM", null, "ICONS.NOTIFY." + (notify ? "ON" : "OFF"), notify ) - .send(interaction.options.getString("name") !== null); + .setFailedMessage("No changes were made", "Success", "PUNISH.NICKNAME.GREEN") + .send(!firstRun); + firstRun = false; if (confirmation.cancelled) timedOut = true; - else if (confirmation.success) success = true; + else if (confirmation.success !== undefined) success = true; else if (confirmation.components) { - notify = confirmation.components.notify.active; + notify = confirmation.components['notify']!.active; + createAppealTicket = confirmation.components["appeal"]!.active; } - } - if (timedOut) { - return await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setEmoji("PUNISH.NICKNAME.GREEN") - .setTitle("Nickname") - .setDescription("No changes were made") - .setStatus("Success") - ], - components: [] - }); - } - let dmd = false; - let dm; + } while (!timedOut && !success); + if (timedOut || !success) return; + let dmSent = false; + let dmMessage: Message; + const config = await client.database.guilds.read(interaction.guild!.id); try { if (notify) { - dm = await (interaction.options.getMember("user") as GuildMember).send({ + const messageData: { + embeds: EmojiEmbed[]; + components: ActionRowBuilder[]; + } = { embeds: [ new EmojiEmbed() .setEmoji("PUNISH.NICKNAME.RED") .setTitle("Nickname changed") .setDescription( - `Your nickname was ${interaction.options.getString("name") ? "changed" : "cleared"} in ${ - interaction.guild.name + `Your nickname was ${interaction.options.get("name")?.value as string ? "changed" : "cleared"} in ${ + interaction.guild!.name }.` + - (interaction.options.getString("name") - ? ` it is now: ${interaction.options.getString("name")}` + (interaction.options.get("name")?.value as string + ? ` it is now: ${interaction.options.get("name")?.value as string}` : "") + "\n\n" + - (confirmation.components.appeal.response - ? `You can appeal this here: <#${confirmation.components.appeal.response}>` + (createAppealTicket + ? `You can appeal this in the ticket created in <#${confirmation.components!["appeal"]!.response}>` : "") ) .setStatus("Danger") - ] - }); - dmd = true; + ], components: [] + }; + if (config.moderation.nick.text && config.moderation.nick.link) { + messageData.embeds[0]!.setFooter(LinkWarningFooter) + messageData.components.push(new ActionRowBuilder() + .addComponents(new ButtonBuilder() + .setStyle(ButtonStyle.Link) + .setLabel(config.moderation.nick.text) + .setURL(config.moderation.nick.link.replaceAll("{id}", (interaction.options.getMember("user") as GuildMember).id)) + ) + ) + } + dmMessage = await (interaction.options.getMember("user") as GuildMember).send(messageData); + dmSent = true; } } catch { - dmd = false; + dmSent = false; } + let member: GuildMember; + let before: string | null; + let nickname: string | undefined; try { - const member = interaction.options.getMember("user") as GuildMember; - const before = member.nickname; - const nickname = interaction.options.getString("name"); + member = interaction.options.getMember("user") as GuildMember; + before = member.nickname; + nickname = interaction.options.get("name")?.value as string | undefined; member.setNickname(nickname ?? null, "Nucleus Nickname command"); await client.database.history.create( "nickname", - interaction.guild.id, + interaction.guild!.id, member.user, interaction.user, null, before, nickname ); - const { log, NucleusColors, entry, renderUser, renderDelta } = client.logger; - const data = { - meta: { - type: "memberUpdate", - displayName: "Member Updated", - calculateType: "guildMemberUpdate", - color: NucleusColors.yellow, - emoji: "PUNISH.NICKNAME.YELLOW", - timestamp: new Date().getTime() - }, - list: { - memberId: entry(member.id, `\`${member.id}\``), - before: entry(before, before ? before : "*None*"), - after: entry(nickname, nickname ? nickname : "*None*"), - updated: entry(new Date().getTime(), renderDelta(new Date().getTime())), - updatedBy: entry(interaction.user.id, renderUser(interaction.user)) - }, - hidden: { - guild: interaction.guild.id - } - }; - log(data); } catch { await interaction.editReply({ embeds: [ @@ -145,10 +146,31 @@ const callback = async (interaction: CommandInteraction): Promise => { ], components: [] }); - if (dmd) await dm.delete(); + if (dmSent) await dmMessage!.delete(); return; } - const failed = !dmd && notify; + const data = { + meta: { + type: "memberUpdate", + displayName: "Member Updated", + calculateType: "guildMemberUpdate", + color: NucleusColors.yellow, + emoji: "PUNISH.NICKNAME.YELLOW", + timestamp: new Date().getTime() + }, + list: { + memberId: entry(member.id, `\`${member.id}\``), + before: entry(before, before ?? "*No nickname set*"), + after: entry(nickname ?? null, nickname ?? "*No nickname set*"), + updated: entry(new Date().getTime(), renderDelta(new Date().getTime())), + updatedBy: entry(interaction.user.id, renderUser(interaction.user)) + }, + hidden: { + guild: interaction.guild!.id + } + }; + log(data); + const failed = !dmSent && notify; await interaction.editReply({ embeds: [ new EmojiEmbed() @@ -157,8 +179,8 @@ const callback = async (interaction: CommandInteraction): Promise => { .setDescription( "The members nickname was changed" + (failed ? ", but was not notified" : "") + - (confirmation.components.appeal.response - ? ` and an appeal ticket was opened in <#${confirmation.components.appeal.response}>` + (confirmation.components!["appeal"]!.response !== null + ? ` and an appeal ticket was opened in <#${confirmation.components!["appeal"]!.response}>` : "") ) .setStatus(failed ? "Warning" : "Success") diff --git a/src/commands/mod/warn.ts b/src/commands/mod/warn.ts index 8e96078..38aa4ad 100644 --- a/src/commands/mod/warn.ts +++ b/src/commands/mod/warn.ts @@ -67,7 +67,7 @@ const callback = async (interaction: CommandInteraction): Promise => { createAppealTicket = confirmation.components["appeal"]!.active; } } while (!timedOut && !success) - if (timedOut || !confirmation.success) return; + if (timedOut || !success) return; let dmSent = false; const config = await client.database.guilds.read(interaction.guild.id); try { diff --git a/src/events/channelUpdate.ts b/src/events/channelUpdate.ts index df212a2..67577f8 100644 --- a/src/events/channelUpdate.ts +++ b/src/events/channelUpdate.ts @@ -1,39 +1,40 @@ +import { GuildChannel, AuditLogEvent } from 'discord.js'; import humanizeDuration from "humanize-duration"; +import type { NucleusClient } from "../utils/client.js"; import getEmojiByName from "../utils/getEmojiByName.js"; export const event = "channelUpdate"; -export async function callback(client, oc, nc) { - const config = await client.memory.readGuildInfo(nc.guild.id); - return; +export async function callback(client: NucleusClient, oldChannel: GuildChannel, newChannel: GuildChannel) { + const config = await client.memory.readGuildInfo(newChannel.guild.id); const { getAuditLog, log, NucleusColors, entry, renderDelta, renderUser, renderChannel } = client.logger; - if (nc.parent && nc.parent.id === config.tickets.category) return; + if (newChannel.parent && newChannel.parent.id === config.tickets.category) return; - const auditLog = await getAuditLog(nc.guild, "CHANNEL_UPDATE"); - const audit = auditLog.entries.filter((entry) => entry.target.id === nc.id).first(); + const auditLog = await getAuditLog(newChannel.guild, "CHANNEL_UPDATE"); + const audit = auditLog.entries.filter((entry) => entry.target.id === newChannel.id).first(); if (audit.executor.id === client.user.id) return; let emoji: string; let readableType: string; let displayName: string; const changes = { - channelId: entry(nc.id, `\`${nc.id}\``), - channel: entry(nc.id, renderChannel(nc)), + channelId: entry(newChannel.id, `\`${newChannel.id}\``), + channel: entry(newChannel.id, renderChannel(newChannel)), edited: entry(new Date().getTime(), renderDelta(new Date().getTime())), - editedBy: entry(audit.executor.id, renderUser((await nc.guild.members.fetch(audit.executor.id)).user)) + editedBy: entry(audit.executor.id, renderUser((await newChannel.guild.members.fetch(audit.executor.id)).user)) }; - if (oc.name !== nc.name) changes.name = entry([oc.name, nc.name], `${oc.name} -> ${nc.name}`); - if (oc.position !== nc.position) - changes.position = entry([oc.position, nc.position], `${oc.position} -> ${nc.position}`); + if (oldChannel.name !== newChannel.name) changes.name = entry([oldChannel.name, newChannel.name], `${oldChannel.name} -> ${newChannel.name}`); + if (oldChannel.position !== newChannel.position) + changes.position = entry([oldChannel.position, newChannel.position], `${oldChannel.position} -> ${newChannel.position}`); - switch (nc.type) { + switch (newChannel.type) { case "GUILD_TEXT": { emoji = "CHANNEL.TEXT.EDIT"; readableType = "Text"; displayName = "Text Channel"; - let oldTopic = oc.topic, - newTopic = nc.topic; + let oldTopic = oldChannel.topic, + newTopic = newChannel.topic; if (oldTopic) { if (oldTopic.length > 256) oldTopic = `\`\`\`\n${oldTopic.replace("`", "'").substring(0, 253) + "..."}\n\`\`\``; @@ -49,15 +50,15 @@ export async function callback(client, oc, nc) { newTopic = "None"; } const nsfw = ["", ""]; - nsfw[0] = oc.nsfw ? `${getEmojiByName("CONTROL.TICK")} Yes` : `${getEmojiByName("CONTROL.CROSS")} No`; - nsfw[1] = nc.nsfw ? `${getEmojiByName("CONTROL.TICK")} Yes` : `${getEmojiByName("CONTROL.CROSS")} No`; - if (oc.topic !== nc.topic) - changes.description = entry([oc.topic, nc.topic], `\nBefore: ${oldTopic}\nAfter: ${newTopic}`); - if (oc.nsfw !== nc.nsfw) changes.nsfw = entry([oc.nsfw, nc.nsfw], `${nsfw[0]} -> ${nsfw[1]}`); - if (oc.rateLimitPerUser !== nc.rateLimitPerUser) + nsfw[0] = oldChannel.nsfw ? `${getEmojiByName("CONTROL.TICK")} Yes` : `${getEmojiByName("CONTROL.CROSS")} No`; + nsfw[1] = newChannel.nsfw ? `${getEmojiByName("CONTROL.TICK")} Yes` : `${getEmojiByName("CONTROL.CROSS")} No`; + if (oldChannel.topic !== newChannel.topic) + changes.description = entry([oldChannel.topic, newChannel.topic], `\nBefore: ${oldTopic}\nAfter: ${newTopic}`); + if (oldChannel.nsfw !== newChannel.nsfw) changes.nsfw = entry([oldChannel.nsfw, newChannel.nsfw], `${nsfw[0]} -> ${nsfw[1]}`); + if (oldChannel.rateLimitPerUser !== newChannel.rateLimitPerUser) changes.rateLimitPerUser = entry( - [oc.rateLimitPerUser, nc.rateLimitPerUser], - `${humanizeDuration(oc.rateLimitPerUser * 1000)} -> ${humanizeDuration(nc.rateLimitPerUser * 1000)}` + [oldChannel.rateLimitPerUser, newChannel.rateLimitPerUser], + `${humanizeDuration(oldChannel.rateLimitPerUser * 1000)} -> ${humanizeDuration(newChannel.rateLimitPerUser * 1000)}` ); break; @@ -66,8 +67,8 @@ export async function callback(client, oc, nc) { emoji = "CHANNEL.TEXT.EDIT"; readableType = "News"; displayName = "News Channel"; - let oldTopic = oc.topic, - newTopic = nc.topic; + let oldTopic = oldChannel.topic, + newTopic = newChannel.topic; if (oldTopic) { if (oldTopic.length > 256) oldTopic = `\`\`\`\n${oldTopic.replace("`", "'").substring(0, 253) + "..."}\n\`\`\``; @@ -82,25 +83,25 @@ export async function callback(client, oc, nc) { } else { newTopic = "None"; } - if (oc.nsfw !== nc.nsfw) - changes.nsfw = entry([oc.nsfw, nc.nsfw], `${oc.nsfw ? "On" : "Off"} -> ${nc.nsfw ? "On" : "Off"}`); + if (oldChannel.nsfw !== newChannel.nsfw) + changes.nsfw = entry([oldChannel.nsfw, newChannel.nsfw], `${oldChannel.nsfw ? "On" : "Off"} -> ${newChannel.nsfw ? "On" : "Off"}`); break; } case "GUILD_VOICE": { emoji = "CHANNEL.VOICE.EDIT"; readableType = "Voice"; displayName = "Voice Channel"; - if (oc.bitrate !== nc.bitrate) - changes.bitrate = entry([oc.bitrate, nc.bitrate], `${oc.bitrate} -> ${nc.bitrate}`); - if (oc.userLimit !== nc.userLimit) + if (oldChannel.bitrate !== newChannel.bitrate) + changes.bitrate = entry([oldChannel.bitrate, newChannel.bitrate], `${oldChannel.bitrate} -> ${newChannel.bitrate}`); + if (oldChannel.userLimit !== newChannel.userLimit) changes.maxUsers = entry( - [oc.userLimit, nc.userLimit], - `${oc.userLimit ? oc.userLimit : "Unlimited"} -> ${nc.userLimit}` + [oldChannel.userLimit, newChannel.userLimit], + `${oldChannel.userLimit ? oldChannel.userLimit : "Unlimited"} -> ${newChannel.userLimit}` ); - if (oc.rtcRegion !== nc.rtcRegion) + if (oldChannel.rtcRegion !== newChannel.rtcRegion) changes.region = entry( - [oc.rtcRegion, nc.rtcRegion], - `${oc.rtcRegion || "Automatic"} -> ${nc.rtcRegion || "Automatic"}` + [oldChannel.rtcRegion, newChannel.rtcRegion], + `${oldChannel.rtcRegion || "Automatic"} -> ${newChannel.rtcRegion || "Automatic"}` ); break; } @@ -108,8 +109,8 @@ export async function callback(client, oc, nc) { emoji = "CHANNEL.VOICE.EDIT"; readableType = "Stage"; displayName = "Stage Channel"; - let oldTopic = oc.topic, - newTopic = nc.topic; + let oldTopic = oldChannel.topic, + newTopic = newChannel.topic; if (oldTopic) { if (oldTopic.length > 256) oldTopic = `\`\`\`\n${oldTopic.replace("`", "'").substring(0, 253) + "..."}\n\`\`\``; @@ -124,17 +125,17 @@ export async function callback(client, oc, nc) { } else { newTopic = "None"; } - if (oc.bitrate !== nc.bitrate) - changes.bitrate = entry([oc.bitrate, nc.bitrate], `${oc.bitrate} -> ${nc.bitrate}`); - if (oc.userLimit !== nc.userLimit) + if (oldChannel.bitrate !== newChannel.bitrate) + changes.bitrate = entry([oldChannel.bitrate, newChannel.bitrate], `${oldChannel.bitrate} -> ${newChannel.bitrate}`); + if (oldChannel.userLimit !== newChannel.userLimit) changes.maxUsers = entry( - [oc.userLimit, nc.userLimit], - `${oc.userLimit ? oc.userLimit : "Unlimited"} -> ${nc.userLimit}` + [oldChannel.userLimit, newChannel.userLimit], + `${oldChannel.userLimit ? oldChannel.userLimit : "Unlimited"} -> ${newChannel.userLimit}` ); - if (oc.rtcRegion !== nc.rtcRegion) + if (oldChannel.rtcRegion !== newChannel.rtcRegion) changes.region = entry( - [oc.rtcRegion, nc.rtcRegion], - `${oc.rtcRegion || "Automatic"} -> ${nc.rtcRegion || "Automatic"}` + [oldChannel.rtcRegion, newChannel.rtcRegion], + `${oldChannel.rtcRegion || "Automatic"} -> ${newChannel.rtcRegion || "Automatic"}` ); break; } @@ -150,9 +151,9 @@ export async function callback(client, oc, nc) { displayName = "Channel"; } } - const t = oc.type.split("_")[1]; - if (oc.type !== nc.type) - changes.type = entry([oc.type, nc.type], `${t[0] + t.splice(1).toLowerCase()} -> ${readableType}`); + const t = oldChannel.type.split("_")[1]; + if (oldChannel.type !== newChannel.type) + changes.type = entry([oldChannel.type, newChannel.type], `${t[0] + t.splice(1).toLowerCase()} -> ${readableType}`); if (!(Object.values(changes).length - 4)) return; const data = { meta: { @@ -165,7 +166,7 @@ export async function callback(client, oc, nc) { }, list: changes, hidden: { - guild: nc.guild.id + guild: newChannel.guild.id } }; log(data); diff --git a/src/events/emojiCreate.ts b/src/events/emojiCreate.ts index b4e9dfa..8023abc 100644 --- a/src/events/emojiCreate.ts +++ b/src/events/emojiCreate.ts @@ -1,10 +1,14 @@ +import { AuditLogEvent } from 'discord.js'; +import type { NucleusClient } from "../utils/client.js"; +import type { GuildEmoji, GuildAuditLogsEntry } from 'discord.js' export const event = "emojiCreate"; -export async function callback(client, emoji) { - const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta, renderEmoji } = emoji.client.logger; - const auditLog = await getAuditLog(emoji.guild, "EMOJI_CREATE"); - const audit = auditLog.entries.filter((entry) => entry.target.id === emoji.id).first(); - if (audit.executor.id === client.user.id) return; +export async function callback(client: NucleusClient, emoji: GuildEmoji) { + const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta, renderEmoji } = client.logger; + const auditLog = (await getAuditLog(emoji.guild, AuditLogEvent.EmojiCreate)) + .filter((entry: GuildAuditLogsEntry) => (entry.target as GuildEmoji)!.id === emoji.id)[0]; + if (!auditLog) return; + if (auditLog.executor!.id === client.user!.id) return; const data = { meta: { type: "emojiCreate", @@ -17,7 +21,7 @@ export async function callback(client, emoji) { list: { emojiId: entry(emoji.id, `\`${emoji.id}\``), emoji: entry(emoji.name, renderEmoji(emoji)), - createdBy: entry(audit.executor.id, renderUser(audit.executor)), + createdBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!)), created: entry(emoji.createdTimestamp, renderDelta(emoji.createdTimestamp)) }, hidden: { @@ -26,3 +30,4 @@ export async function callback(client, emoji) { }; log(data); } + diff --git a/src/events/emojiDelete.ts b/src/events/emojiDelete.ts index c4b1535..f607cf4 100644 --- a/src/events/emojiDelete.ts +++ b/src/events/emojiDelete.ts @@ -1,10 +1,14 @@ +import { AuditLogEvent } from 'discord.js'; +import type { NucleusClient } from "../utils/client.js"; +import type { GuildEmoji, GuildAuditLogsEntry } from 'discord.js' export const event = "emojiDelete"; -export async function callback(client, emoji) { - const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta, renderEmoji } = emoji.client.logger; - const auditLog = await getAuditLog(emoji.guild, "EMOJI_DELETE"); - const audit = auditLog.entries.filter((entry) => entry.target.id === emoji.id).first(); - if (audit.executor.id === client.user.id) return; +export async function callback(client: NucleusClient, emoji: GuildEmoji) { + const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta, renderEmoji } = client.logger; + const auditLog = (await getAuditLog(emoji.guild, AuditLogEvent.EmojiCreate)) + .filter((entry: GuildAuditLogsEntry) => (entry.target as GuildEmoji)!.id === emoji.id)[0]; + if (!auditLog) return; + if (auditLog.executor!.id === client.user!.id) return; const data = { meta: { type: "emojiDelete", @@ -12,14 +16,14 @@ export async function callback(client, emoji) { calculateType: "emojiUpdate", color: NucleusColors.red, emoji: "GUILD.EMOJI.DELETE", - timestamp: audit.createdTimestamp + timestamp: auditLog.createdTimestamp }, list: { emojiId: entry(emoji.id, `\`${emoji.id}\``), emoji: entry(emoji.name, renderEmoji(emoji)), - deletedBy: entry(audit.executor.id, renderUser(audit.executor)), + deletedBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!)), created: entry(emoji.createdTimestamp, renderDelta(emoji.createdTimestamp)), - deleted: entry(audit.createdTimestamp, renderDelta(audit.createdTimestamp)) + deleted: entry(auditLog.createdTimestamp, renderDelta(auditLog.createdTimestamp)) }, hidden: { guild: emoji.guild.id diff --git a/src/events/emojiUpdate.ts b/src/events/emojiUpdate.ts index 66227d4..201dd42 100644 --- a/src/events/emojiUpdate.ts +++ b/src/events/emojiUpdate.ts @@ -1,19 +1,22 @@ +import { AuditLogEvent } from 'discord.js'; +import type { NucleusClient } from "../utils/client.js"; +import type { GuildEmoji, GuildAuditLogsEntry } from 'discord.js' export const event = "emojiUpdate"; -export async function callback(client, oe, ne) { +export async function callback(client: NucleusClient, oldEmoji: GuildEmoji, newEmoji: GuildEmoji) { const { getAuditLog, log, NucleusColors, entry, renderDelta, renderUser, renderEmoji } = client.logger; - if (oe.name === ne.name) return; - const auditLog = await getAuditLog(ne.guild, "EMOJI_UPDATE"); - const audit = auditLog.entries.first(); - if (audit.executor.id === client.user.id) return; + const auditLog = (await getAuditLog(newEmoji.guild, AuditLogEvent.EmojiCreate)) + .filter((entry: GuildAuditLogsEntry) => (entry.target as GuildEmoji)!.id === newEmoji.id)[0]; + if (!auditLog) return; + if (auditLog.executor!.id === client.user!.id) return; const changes = { - emojiId: entry(ne.id, `\`${ne.id}\``), - emoji: entry(ne.id, renderEmoji(ne)), - edited: entry(ne.createdTimestamp, renderDelta(ne.createdTimestamp)), - editedBy: entry(audit.executor.id, renderUser((await ne.guild.members.fetch(audit.executor.id)).user)), - name: entry([oe.name, ne.name], `\`:${oe.name}:\` -> \`:${ne.name}:\``) + emojiId: entry(newEmoji.id, `\`${newEmoji.id}\``), + emoji: entry(newEmoji.id, renderEmoji(newEmoji)), + edited: entry(newEmoji.createdTimestamp, renderDelta(newEmoji.createdTimestamp)), + editedBy: entry(auditLog.executor!.id, renderUser((await newEmoji.guild.members.fetch(auditLog.executor!.id)).user)), + name: entry([oldEmoji.name!, newEmoji.name!], `\`:${oldEmoji.name}:\` -> \`:${newEmoji.name}:\``) }; const data = { meta: { @@ -22,11 +25,11 @@ export async function callback(client, oe, ne) { calculateType: "emojiUpdate", color: NucleusColors.yellow, emoji: "GUILD.EMOJI.EDIT", - timestamp: audit.createdTimestamp + timestamp: auditLog.createdTimestamp }, list: changes, hidden: { - guild: ne.guild.id + guild: newEmoji.guild.id } }; log(data); diff --git a/src/events/guildBanAdd.ts b/src/events/guildBanAdd.ts index 5fd0b49..3d96245 100644 --- a/src/events/guildBanAdd.ts +++ b/src/events/guildBanAdd.ts @@ -1,4 +1,5 @@ -import type { GuildAuditLogsEntry, GuildBan } from "discord.js"; +import type { GuildAuditLogsEntry, GuildBan, User } from "discord.js"; +import { AuditLogEvent } from 'discord.js'; import { purgeByUser } from "../actions/tickets/delete.js"; import { callback as statsChannelRemove } from "../reflex/statsChannelUpdate.js"; import type { NucleusClient } from "../utils/client.js"; @@ -7,12 +8,13 @@ export const event = "guildBanAdd"; export async function callback(client: NucleusClient, ban: GuildBan) { await statsChannelRemove(client, undefined, ban.guild, ban.user); - purgeByUser(ban.user.id, ban.guild); + purgeByUser(ban.user.id, ban.guild.id); const { log, NucleusColors, entry, renderUser, renderDelta, getAuditLog } = client.logger; - const auditLog = await getAuditLog(ban.guild, "MEMBER_BAN_ADD"); - const audit = auditLog.entries.filter((entry: GuildAuditLogsEntry) => entry.target!.id === ban.user.id).first(); - if (audit.executor.id === client.user.id) return; - await client.database.history.create("ban", ban.guild.id, ban.user, audit.executor, audit.reason); + const auditLog: GuildAuditLogsEntry | undefined = (await getAuditLog(ban.guild, AuditLogEvent.EmojiCreate)) + .filter((entry: GuildAuditLogsEntry) => ((entry.target! as User).id === ban.user.id))[0]; + if (!auditLog) return; + if (auditLog.executor!.id === client.user!.id) return; + await client.database.history.create("ban", ban.guild.id, ban.user, auditLog.executor, auditLog.reason); const data = { meta: { type: "memberBan", @@ -26,9 +28,9 @@ export async function callback(client: NucleusClient, ban: GuildBan) { memberId: entry(ban.user.id, `\`${ban.user.id}\``), name: entry(ban.user.id, renderUser(ban.user)), banned: entry(new Date().getTime(), renderDelta(new Date().getTime())), - bannedBy: entry(audit.executor.id, renderUser(audit.executor)), - reason: entry(audit.reason, audit.reason ? `\n> ${audit.reason}` : "*No reason provided.*"), - accountCreated: entry(ban.user.createdAt, renderDelta(ban.user.createdAt)), + bannedBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!)), + reason: entry(auditLog.reason, auditLog.reason ? `\n> ${auditLog.reason}` : "*No reason provided.*"), + accountCreated: entry(ban.user.createdTimestamp, renderDelta(ban.user.createdTimestamp)), serverMemberCount: ban.guild.memberCount }, hidden: { diff --git a/src/events/guildBanRemove.ts b/src/events/guildBanRemove.ts index f9b427a..bcb70d5 100644 --- a/src/events/guildBanRemove.ts +++ b/src/events/guildBanRemove.ts @@ -1,16 +1,18 @@ -import type { GuildAuditLogsEntry, GuildBan } from "discord.js"; +import type { GuildAuditLogsEntry, GuildBan, User } from "discord.js"; +import { AuditLogEvent } from "discord.js"; import { purgeByUser } from "../actions/tickets/delete.js"; import type { NucleusClient } from "../utils/client.js"; export const event = "guildBanRemove"; export async function callback(client: NucleusClient, ban: GuildBan) { - purgeByUser(ban.user.id, ban.guild); + purgeByUser(ban.user.id, ban.guild.id); const { log, NucleusColors, entry, renderUser, renderDelta, getAuditLog } = client.logger; - const auditLog = await getAuditLog(ban.guild, "MEMBER_BAN_REMOVE"); - const audit = auditLog.entries.filter((entry: GuildAuditLogsEntry) => entry.target!.id === ban.user.id).first(); - if (audit.executor.id === client.user.id) return; - await client.database.history.create("unban", ban.guild.id, ban.user, audit.executor, audit.reason); + const auditLog = (await getAuditLog(ban.guild, AuditLogEvent.EmojiCreate)) + .filter((entry: GuildAuditLogsEntry) => ((entry.target! as User).id === ban.user.id))[0]; + if (!auditLog) return; + if (auditLog.executor!.id === client.user!.id) return; + await client.database.history.create("unban", ban.guild.id, ban.user, auditLog.executor, auditLog.reason); const data = { meta: { type: "memberUnban", @@ -24,8 +26,8 @@ export async function callback(client: NucleusClient, ban: GuildBan) { memberId: entry(ban.user.id, `\`${ban.user.id}\``), name: entry(ban.user.id, renderUser(ban.user)), unbanned: entry(new Date().getTime(), renderDelta(new Date().getTime())), - unbannedBy: entry(audit.executor.id, renderUser(audit.executor)), - accountCreated: entry(ban.user.createdAt, renderDelta(ban.user.createdAt)) + unbannedBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!)), + accountCreated: entry(ban.user.createdTimestamp, renderDelta(ban.user.createdTimestamp)) }, hidden: { guild: ban.guild.id diff --git a/src/premium/attachmentLogs.ts b/src/premium/attachmentLogs.ts index abda27d..8cfe080 100644 --- a/src/premium/attachmentLogs.ts +++ b/src/premium/attachmentLogs.ts @@ -1,3 +1,4 @@ +import { getCommandMentionByName } from './../utils/getCommandMentionByName'; import client from "../utils/client.js"; import keyValueList from "../utils/generateKeyValueList.js"; import singleNotify from "../utils/singleNotify.js"; @@ -37,7 +38,7 @@ export default async function logAttachment(message: Message): Promise { let c: GuildTextBasedChannel | null = guild.publicUpdatesChannel ? guild.publicUpdatesChannel : guild.systemChannel; @@ -50,14 +23,14 @@ export default async (guild: Guild, interaction?: CommandInteraction) => { : (guild.channels.cache.find( (ch) => [ - "GUILD_TEXT", - "GUILD_NEWS", - "GUILD_NEWS_THREAD", - "GUILD_PRIVATE_THREAD", - "GUILD_PUBLIC_THREAD" + ChannelType.GuildText, + ChannelType.GuildAnnouncement, + ChannelType.PublicThread, + ChannelType.PrivateThread, + ChannelType.AnnouncementThread ].includes(ch.type) && - ch.permissionsFor(guild.roles.everyone).has("SEND_MESSAGES") && - ch.permissionsFor(guild.me!).has("EMBED_LINKS") + ch.permissionsFor(guild.roles.everyone).has("SendMessages") && + ch.permissionsFor(guild.members.me!).has("EmbedLinks") ) as GuildTextBasedChannel | undefined) ?? null; if (interaction) c = interaction.channel as GuildTextBasedChannel; if (!c) { @@ -226,7 +199,7 @@ export default async (guild: Guild, interaction?: CommandInteraction) => { let page = 0; const publicFilter = async (component: MessageComponentInteraction) => { - return (component.member as Discord.GuildMember).permissions.has("MANAGE_GUILD"); + return (component.member as Discord.GuildMember).permissions.has("ManageGuild"); }; let selectPaneOpen = false; @@ -234,20 +207,20 @@ export default async (guild: Guild, interaction?: CommandInteraction) => { let cancelled = false; let timedOut = false; while (!cancelled && !timedOut) { - let selectPane: ActionRowBuilder[] = []; + let selectPane: ActionRowBuilder[] = []; if (selectPaneOpen) { - const options: MessageSelectOptionData[] = []; + const options: Discord.StringSelectMenuOptionBuilder[] = []; pages.forEach((embed) => { - options.push({ - label: embed.title, - value: embed.pageId.toString(), - description: embed.description || "" - }); + options.push(new Discord.StringSelectMenuOptionBuilder() + .setLabel(embed.title) + .setValue(embed.pageId.toString()) + .setDescription(embed.description || "") + ); }); selectPane = [ - new ActionRowBuilder().addComponents([ - new Discord.SelectMenuBuilder() + new ActionRowBuilder().addComponents([ + new Discord.StringSelectMenuBuilder() .addOptions(options) .setCustomId("page") .setMaxValues(1) @@ -255,8 +228,8 @@ export default async (guild: Guild, interaction?: CommandInteraction) => { ]) ]; } - const components = selectPane.concat([ - new ActionRowBuilder().addComponents([ + const components: ActionRowBuilder[] = selectPane.concat([ + new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId("left") .setEmoji(getEmojiByName("CONTROL.LEFT", "id")) @@ -272,18 +245,18 @@ export default async (guild: Guild, interaction?: CommandInteraction) => { .setEmoji(getEmojiByName("CONTROL.RIGHT", "id")) .setStyle(ButtonStyle.Secondary) .setDisabled(page === pages.length - 1) - ]) + ) ]); if (interaction) { const em = new Discord.EmbedBuilder(pages[page]!.embed); - em.setDescription(em.description + "\n\n" + createPageIndicator(pages.length, page)); + em.setDescription(em.data.description + "\n\n" + createPageIndicator(pages.length, page)); await interaction.editReply({ embeds: [em], components: components }); } else { const em = new Discord.EmbedBuilder(pages[page]!.embed); - em.setDescription(em.description + "\n\n" + createPageIndicator(pages.length, page)); + em.setDescription(em.data.description + "\n\n" + createPageIndicator(pages.length, page)); (await m.edit({ embeds: [em], components: components @@ -313,7 +286,7 @@ export default async (guild: Guild, interaction?: CommandInteraction) => { } else if (i.component.customId === "select") { selectPaneOpen = !selectPaneOpen; } else if (i.component.customId === "page") { - page = parseInt((i as SelectMenuInteraction).values[0]!); + page = parseInt((i as StringSelectMenuInteraction).values[0]!); selectPaneOpen = false; } else { cancelled = true; @@ -322,7 +295,7 @@ export default async (guild: Guild, interaction?: CommandInteraction) => { if (timedOut) { if (interaction) { const em = new Discord.EmbedBuilder(pages[page]!.embed); - em.setDescription(em.description + "\n\n" + createPageIndicator(pages.length, page)).setFooter({ + em.setDescription(em.data.description + "\n\n" + createPageIndicator(pages.length, page)).setFooter({ text: "Message timed out" }); await interaction.editReply({ @@ -331,7 +304,7 @@ export default async (guild: Guild, interaction?: CommandInteraction) => { }); } else { const em = new Discord.EmbedBuilder(pages[page]!.embed); - em.setDescription(em.description + "\n\n" + createPageIndicator(pages.length, page)).setFooter({ + em.setDescription(em.data.description + "\n\n" + createPageIndicator(pages.length, page)).setFooter({ text: "Message timed out" }); await m.edit({ @@ -342,7 +315,7 @@ export default async (guild: Guild, interaction?: CommandInteraction) => { } else { if (interaction) { const em = new Discord.EmbedBuilder(pages[page]!.embed); - em.setDescription(em.description + "\n\n" + createPageIndicator(pages.length, page)); + em.setDescription(em.data.description + "\n\n" + createPageIndicator(pages.length, page)); em.setFooter({ text: "Message closed" }); interaction.editReply({ embeds: [em], diff --git a/src/reflex/statsChannelUpdate.ts b/src/reflex/statsChannelUpdate.ts index 2e12429..db705d9 100644 --- a/src/reflex/statsChannelUpdate.ts +++ b/src/reflex/statsChannelUpdate.ts @@ -1,3 +1,4 @@ +import { getCommandMentionByName } from '../utils/getCommandMentionByName.js'; import type { Guild, User } from "discord.js"; import type { NucleusClient } from "../utils/client.js"; import type { GuildMember } from "discord.js"; @@ -31,7 +32,7 @@ export async function callback(client: NucleusClient, member?: GuildMember, guil return singleNotify( "statsChannelDeleted", guild!.id, - "One or more of your stats channels have been deleted. Please use `/settings stats` if you wish to add the channel again.\n" + + `One or more of your stats channels have been deleted. You can use ${await getCommandMentionByName("settings/stats")}.\n` + `The channels name was: ${deleted!.name}`, "Critical" ); diff --git a/src/reflex/welcome.ts b/src/reflex/welcome.ts index 7722086..87bb81a 100644 --- a/src/reflex/welcome.ts +++ b/src/reflex/welcome.ts @@ -1,3 +1,4 @@ +import { getCommandMentionByName } from './../utils/getCommandMentionByName.js'; import type { NucleusClient } from "../utils/client.js"; import convertCurlyBracketString from "../utils/convertCurlyBracketString.js"; import client from "../utils/client.js"; @@ -26,7 +27,7 @@ export async function callback(_client: NucleusClient, member: GuildMember) { }); } else { const channel: GuildChannel | null = await member.guild.channels.fetch(config.welcome.channel) as GuildChannel | null; - if (!channel) return; // TODO: SEN + if (!channel) return await singleNotify("welcomeChannelDeleted", member.guild.id, `The welcome channel has been deleted or is no longer accessible. Use ${await getCommandMentionByName("settings/welcome")} to set a new one`, "Warning") if (!(channel instanceof BaseGuildTextChannel)) return; if (channel.guild.id !== member.guild.id) return; try { @@ -38,7 +39,7 @@ export async function callback(_client: NucleusClient, member: GuildMember) { singleNotify( "welcomeChannelDeleted", member.guild.id, - "The welcome channel has been deleted or is no longer accessible. Use /settings welcome to set a new one", + `The welcome channel has been deleted or is no longer accessible. Use ${await getCommandMentionByName("settings/welcome")} to set a new one`, "Warning" ) } diff --git a/src/utils/database.ts b/src/utils/database.ts index a62e148..1e8e990 100644 --- a/src/utils/database.ts +++ b/src/utils/database.ts @@ -313,6 +313,10 @@ export interface GuildConfig { text: null; link: null; }; + nick: { + text: string | null; + link: string | null; + } }; tracks: { name: string; diff --git a/src/utils/log.ts b/src/utils/log.ts index 4565251..d4e2a82 100644 --- a/src/utils/log.ts +++ b/src/utils/log.ts @@ -25,7 +25,7 @@ export const Logger = { const delta = num2 - num1; return `${num1} -> ${num2} (${delta > 0 ? "+" : ""}${delta})`; }, - entry(value: string | number | boolean | null, displayValue: string): { value: string | boolean | null; displayValue: string } { + entry(value: string | number | boolean | null | string[], displayValue: string): { value: string | boolean | null | string[]; displayValue: string } { if (typeof value === "number") value = value.toString(); return { value: value, displayValue: displayValue }; },