blob: 3d6d9121b987b589860bd5edf394a6635d66b96c [file] [log] [blame]
pineafan377794f2022-04-18 19:01:01 +01001import Discord, { CommandInteraction, GuildMember, MessageActionRow, MessageButton } from "discord.js";
pineafan8b4b17f2022-02-27 20:42:52 +00002import { SlashCommandSubcommandBuilder } from "@discordjs/builders";
3import { WrappedCheck } from "jshaiku";
pineafan377794f2022-04-18 19:01:01 +01004import generateEmojiEmbed from "../../utils/generateEmojiEmbed.js";
pineafan8b4b17f2022-02-27 20:42:52 +00005import getEmojiByName from "../../utils/getEmojiByName.js";
6import confirmationMessage from "../../utils/confirmationMessage.js";
7import keyValueList from "../../utils/generateKeyValueList.js";
8import humanizeDuration from "humanize-duration";
pineafan377794f2022-04-18 19:01:01 +01009import { create, areTicketsEnabled } from "../../automations/createModActionTicket.js";
10import readConfig from "../../utils/readConfig.js";
pineafan8b4b17f2022-02-27 20:42:52 +000011
12const command = (builder: SlashCommandSubcommandBuilder) =>
13 builder
14 .setName("mute")
15 .setDescription("Mutes a member using Discord's \"Timeout\" feature")
16 .addUserOption(option => option.setName("user").setDescription("The user to mute").setRequired(true))
17 .addIntegerOption(option => option.setName("days").setDescription("The number of days to mute the user for | Default 0").setMinValue(0).setMaxValue(27).setRequired(false))
18 .addIntegerOption(option => option.setName("hours").setDescription("The number of hours to mute the user for | Default 0").setMinValue(0).setMaxValue(23).setRequired(false))
19 .addIntegerOption(option => option.setName("minutes").setDescription("The number of minutes to mute the user for | Default 0").setMinValue(0).setMaxValue(59).setRequired(false))
20 .addIntegerOption(option => option.setName("seconds").setDescription("The number of seconds to mute the user for | Default 0").setMinValue(0).setMaxValue(59).setRequired(false))
21 .addStringOption(option => option.setName("reason").setDescription("The reason for the mute").setRequired(false))
pineafan377794f2022-04-18 19:01:01 +010022 .addStringOption(option => option.setName("notify").setDescription("If the user should get a message when they are muted | Default yes").setRequired(false)
pineafan5d1908e2022-02-28 21:34:47 +000023 .addChoices([["Yes", "yes"], ["No", "no"]]))
pineafan8b4b17f2022-02-27 20:42:52 +000024
25const callback = async (interaction: CommandInteraction) => {
pineafan377794f2022-04-18 19:01:01 +010026 // @ts-ignore
27 const { log, NucleusColors, renderUser, entry } = interaction.client.logger
pineafan8b4b17f2022-02-27 20:42:52 +000028 const user = interaction.options.getMember("user") as GuildMember
29 const reason = interaction.options.getString("reason")
30 const time = {
31 days: interaction.options.getInteger("days") || 0,
32 hours: interaction.options.getInteger("hours") || 0,
33 minutes: interaction.options.getInteger("minutes") || 0,
34 seconds: interaction.options.getInteger("seconds") || 0
35 }
pineafane625d782022-05-09 18:04:32 +010036 let config = await readConfig(interaction.guild.id)
37 let serverSettingsDescription = (config.moderation.mute.timeout ? "given a timeout" : "")
38 if (config.moderation.mute.role) serverSettingsDescription += (serverSettingsDescription ? " and " : "") + `given the <@&${config.moderation.mute.role}> role`
39
pineafan8b4b17f2022-02-27 20:42:52 +000040 let muteTime = (time.days * 24 * 60 * 60) + (time.hours * 60 * 60) + (time.minutes * 60) + time.seconds
41 if (muteTime == 0) {
42 let m = await interaction.reply({embeds: [
pineafan377794f2022-04-18 19:01:01 +010043 new generateEmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +000044 .setEmoji("PUNISH.MUTE.GREEN")
45 .setTitle("Mute")
46 .setDescription("How long should the user be muted")
47 .setStatus("Success")
48 ], components: [
49 new MessageActionRow().addComponents([
50 new Discord.MessageButton()
51 .setCustomId("1m")
52 .setLabel("1 Minute")
53 .setStyle("SECONDARY"),
54 new Discord.MessageButton()
55 .setCustomId("10m")
56 .setLabel("10 Minutes")
57 .setStyle("SECONDARY"),
58 new Discord.MessageButton()
59 .setCustomId("30m")
60 .setLabel("30 Minutes")
61 .setStyle("SECONDARY"),
62 new Discord.MessageButton()
63 .setCustomId("1h")
64 .setLabel("1 Hour")
65 .setStyle("SECONDARY")
66 ]),
67 new MessageActionRow().addComponents([
68 new Discord.MessageButton()
69 .setCustomId("6h")
70 .setLabel("6 Hours")
71 .setStyle("SECONDARY"),
72 new Discord.MessageButton()
73 .setCustomId("12h")
74 .setLabel("12 Hours")
75 .setStyle("SECONDARY"),
76 new Discord.MessageButton()
77 .setCustomId("1d")
78 .setLabel("1 Day")
79 .setStyle("SECONDARY"),
80 new Discord.MessageButton()
81 .setCustomId("1w")
82 .setLabel("1 Week")
83 .setStyle("SECONDARY")
84 ]),
85 new MessageActionRow().addComponents([
86 new Discord.MessageButton()
87 .setCustomId("cancel")
88 .setLabel("Cancel")
89 .setStyle("DANGER")
90 .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
91 ])
92 ], ephemeral: true, fetchReply: true})
93 let component;
94 try {
95 component = await (m as Discord.Message).awaitMessageComponent({filter: (m) => m.user.id === interaction.user.id, time: 2.5 * 60 * 1000});
96 } catch { return }
97 component.deferUpdate();
pineafan377794f2022-04-18 19:01:01 +010098 if (component.customId == "cancel") return interaction.editReply({embeds: [new generateEmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +000099 .setEmoji("PUNISH.MUTE.RED")
100 .setTitle("Mute")
101 .setDescription("Mute cancelled")
102 .setStatus("Danger")
103 ]})
104 switch (component.customId) {
105 case "1m": { muteTime = 60; break; }
106 case "10m": { muteTime = 60 * 10; break; }
107 case "30m": { muteTime = 60 * 30; break; }
108 case "1h": { muteTime = 60 * 60; break; }
109 case "6h": { muteTime = 60 * 60 * 6; break; }
110 case "12h": { muteTime = 60 * 60 * 12; break; }
111 case "1d": { muteTime = 60 * 60 * 24; break; }
112 case "1w": { muteTime = 60 * 60 * 24 * 7; break; }
113 }
114 } else {
115 await interaction.reply({embeds: [
pineafan377794f2022-04-18 19:01:01 +0100116 new generateEmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000117 .setEmoji("PUNISH.MUTE.GREEN")
118 .setTitle("Mute")
119 .setDescription("Loading...")
120 .setStatus("Success")
121 ], ephemeral: true, fetchReply: true})
122 }
pineafan5d1908e2022-02-28 21:34:47 +0000123 // TODO:[Modals] Replace this with a modal
pineafan377794f2022-04-18 19:01:01 +0100124 let confirmation = await new confirmationMessage(interaction)
pineafan8b4b17f2022-02-27 20:42:52 +0000125 .setEmoji("PUNISH.MUTE.RED")
126 .setTitle("Mute")
127 .setDescription(keyValueList({
128 "user": `<@!${user.id}> (${user.user.username})`,
129 "time": `${humanizeDuration(muteTime * 1000, {round: true})}`,
pineafan5d1908e2022-02-28 21:34:47 +0000130 "reason": `\n> ${reason ? reason : "*No reason provided*"}`
pineafan8b4b17f2022-02-27 20:42:52 +0000131 })
pineafane625d782022-05-09 18:04:32 +0100132 + `The user will be ` + serverSettingsDescription + "\n"
pineafan8b4b17f2022-02-27 20:42:52 +0000133 + `The user **will${interaction.options.getString("notify") === "no" ? ' not' : ''}** be notified\n\n`
134 + `Are you sure you want to mute <@!${(interaction.options.getMember("user") as GuildMember).id}>?`)
135 .setColor("Danger")
136// pluralize("day", interaction.options.getInteger("delete"))
137// const pluralize = (word: string, count: number) => { return count === 1 ? word : word + "s" }
pineafan377794f2022-04-18 19:01:01 +0100138 .send(true)
139 if (confirmation.success) {
pineafan8b4b17f2022-02-27 20:42:52 +0000140 let dmd = false
pineafan5d1908e2022-02-28 21:34:47 +0000141 let dm;
pineafan377794f2022-04-18 19:01:01 +0100142 let config = await readConfig(interaction.guild.id);
pineafan8b4b17f2022-02-27 20:42:52 +0000143 try {
144 if (interaction.options.getString("notify") != "no") {
pineafan5d1908e2022-02-28 21:34:47 +0000145 dm = await (interaction.options.getMember("user") as GuildMember).send({
pineafan377794f2022-04-18 19:01:01 +0100146 embeds: [new generateEmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000147 .setEmoji("PUNISH.MUTE.RED")
148 .setTitle("Muted")
149 .setDescription(`You have been muted in ${interaction.guild.name}` +
pineafan1dc15722022-03-14 21:27:34 +0000150 (interaction.options.getString("reason") ? ` for:\n> ${interaction.options.getString("reason")}` : ".\n\n" +
pineafan8b4b17f2022-02-27 20:42:52 +0000151 `You will be unmuted at: <t:${Math.round((new Date).getTime() / 1000) + muteTime}:D> at <t:${Math.round((new Date).getTime() / 1000) + muteTime}:T> (<t:${Math.round((new Date).getTime() / 1000) + muteTime}:R>)`))
152 .setStatus("Danger")
pineafan377794f2022-04-18 19:01:01 +0100153 ],
154 components: [new MessageActionRow().addComponents(config.moderation.mute.text ? [new MessageButton()
155 .setStyle("LINK")
156 .setLabel(config.moderation.mute.text)
157 .setURL(config.moderation.mute.link)
158 ] : [])]
pineafan8b4b17f2022-02-27 20:42:52 +0000159 })
160 dmd = true
161 }
162 } catch {}
163 try {
pineafane625d782022-05-09 18:04:32 +0100164 if (config.moderation.mute.timeout) {
165 (interaction.options.getMember("user") as GuildMember).timeout(muteTime * 1000, interaction.options.getString("reason") || "No reason provided")
166 }
167 if (config.moderation.mute.role) {
168 (interaction.options.getMember("user") as GuildMember).roles.add(config.moderation.mute.role)
169 }
pineafan8b4b17f2022-02-27 20:42:52 +0000170 } catch {
pineafan377794f2022-04-18 19:01:01 +0100171 await interaction.editReply({embeds: [new generateEmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000172 .setEmoji("PUNISH.MUTE.RED")
173 .setTitle(`Mute`)
pineafan377794f2022-04-18 19:01:01 +0100174 .setDescription("Something went wrong and the user was not mute")
pineafan8b4b17f2022-02-27 20:42:52 +0000175 .setStatus("Danger")
176 ], components: []})
pineafan5d1908e2022-02-28 21:34:47 +0000177 if (dmd) await dm.delete()
178 return
pineafan8b4b17f2022-02-27 20:42:52 +0000179 }
pineafan5d1908e2022-02-28 21:34:47 +0000180 let failed = (dmd == false && interaction.options.getString("notify") != "no")
pineafan377794f2022-04-18 19:01:01 +0100181 await interaction.editReply({embeds: [new generateEmojiEmbed()
pineafan5d1908e2022-02-28 21:34:47 +0000182 .setEmoji(`PUNISH.MUTE.${failed ? "YELLOW" : "GREEN"}`)
183 .setTitle(`Mute`)
184 .setDescription("The member was muted" + (failed ? ", but could not be notified" : ""))
185 .setStatus(failed ? "Warning" : "Success")
186 ], components: []})
pineafan377794f2022-04-18 19:01:01 +0100187 let data = {
188 meta:{
189 type: 'memberMute',
190 displayName: 'Member Muted',
191 calculateType: 'guildMemberPunish',
192 color: NucleusColors.yellow,
193 emoji: 'PUNISH.WARN.YELLOW',
194 timestamp: new Date().getTime()
195 },
196 list: {
197 user: entry((interaction.options.getMember("user") as GuildMember).user.id, renderUser((interaction.options.getMember("user") as GuildMember).user)),
198 mutedBy: entry(interaction.member.user.id, renderUser(interaction.member.user)),
199 time: entry(muteTime, `${humanizeDuration(muteTime * 1000, {round: true})}`),
200 reason: (interaction.options.getString("reason") ? `\n> ${interaction.options.getString("reason")}` : "No reason provided")
201 },
202 hidden: {
203 guild: interaction.guild.id
204 }
205 }
206 log(data, interaction.client);
pineafan8b4b17f2022-02-27 20:42:52 +0000207 } else {
pineafan377794f2022-04-18 19:01:01 +0100208 await interaction.editReply({embeds: [new generateEmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000209 .setEmoji("PUNISH.MUTE.GREEN")
210 .setTitle(`Mute`)
211 .setDescription("No changes were made")
212 .setStatus("Success")
213 ], components: []})
214 }
215}
216
217const check = (interaction: CommandInteraction, defaultCheck: WrappedCheck) => {
pineafan5d1908e2022-02-28 21:34:47 +0000218 let member = (interaction.member as GuildMember)
219 let me = (interaction.guild.me as GuildMember)
220 let apply = (interaction.options.getMember("user") as GuildMember)
221 if (member == null || me == null || apply == null) throw "That member is not in the server"
222 let memberPos = member.roles ? member.roles.highest.position : 0
223 let mePos = me.roles ? me.roles.highest.position : 0
224 let applyPos = apply.roles ? apply.roles.highest.position : 0
pineafan8b4b17f2022-02-27 20:42:52 +0000225 // Check if Nucleus can mute the member
pineafan5d1908e2022-02-28 21:34:47 +0000226 if (! (mePos > applyPos)) throw "I do not have a role higher than that member"
pineafan8b4b17f2022-02-27 20:42:52 +0000227 // Check if Nucleus has permission to mute
228 if (! interaction.guild.me.permissions.has("MODERATE_MEMBERS")) throw "I do not have the `moderate_members` permission";
229 // Do not allow the user to have admin or be the owner
230 if ((interaction.options.getMember("user") as GuildMember).permissions.has("ADMINISTRATOR") || (interaction.options.getMember("user") as GuildMember).id == interaction.guild.ownerId) throw "You cannot mute an admin or the owner"
231 // Do not allow muting Nucleus
232 if ((interaction.member as GuildMember).id == interaction.guild.me.id) throw "I cannot mute myself"
233 // Allow the owner to mute anyone
234 if ((interaction.member as GuildMember).id == interaction.guild.ownerId) return true
235 // Check if the user has moderate_members permission
236 if (! (interaction.member as GuildMember).permissions.has("MODERATE_MEMBERS")) throw "You do not have the `moderate_members` permission";
237 // Check if the user is below on the role list
pineafan5d1908e2022-02-28 21:34:47 +0000238 if (! (memberPos > applyPos)) throw "You do not have a role higher than that member"
pineafan8b4b17f2022-02-27 20:42:52 +0000239 // Allow mute
240 return true
241}
242
243export { command, callback, check };