blob: 151ff76b212a74978db82ed273e8c5756c9ec9d9 [file] [log] [blame]
pineafan63fc5e22022-08-04 22:04:10 +01001import { LoadingEmbed } from "./../../utils/defaultEmbeds.js";
Skyler Grey11236ba2022-08-08 21:13:33 +01002import Discord, { CommandInteraction, GuildMember, Message, MessageActionRow, MessageButton } from "discord.js";
pineafan8b4b17f2022-02-27 20:42:52 +00003import { SlashCommandSubcommandBuilder } from "@discordjs/builders";
pineafan4edb7762022-06-26 19:21:04 +01004import EmojiEmbed 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";
pineafan6702cef2022-06-13 17:52:37 +01009import client from "../../utils/client.js";
Skyler Grey11236ba2022-08-08 21:13:33 +010010import { areTicketsEnabled, create } from "../../actions/createModActionTicket.js";
pineafan8b4b17f2022-02-27 20:42:52 +000011
12const command = (builder: SlashCommandSubcommandBuilder) =>
13 builder
pineafan63fc5e22022-08-04 22:04:10 +010014 .setName("mute")
Skyler Grey11236ba2022-08-08 21:13:33 +010015 .setDescription("Mutes a member, stopping them from talking in the server")
16 .addUserOption((option) => option.setName("user").setDescription("The user to mute").setRequired(true))
Skyler Grey75ea9172022-08-06 10:22:23 +010017 .addIntegerOption((option) =>
18 option
19 .setName("days")
Skyler Grey11236ba2022-08-08 21:13:33 +010020 .setDescription("The number of days to mute the user for | Default: 0")
Skyler Grey75ea9172022-08-06 10:22:23 +010021 .setMinValue(0)
22 .setMaxValue(27)
23 .setRequired(false)
24 )
25 .addIntegerOption((option) =>
26 option
27 .setName("hours")
Skyler Grey11236ba2022-08-08 21:13:33 +010028 .setDescription("The number of hours to mute the user for | Default: 0")
Skyler Grey75ea9172022-08-06 10:22:23 +010029 .setMinValue(0)
30 .setMaxValue(23)
31 .setRequired(false)
32 )
33 .addIntegerOption((option) =>
34 option
35 .setName("minutes")
Skyler Grey11236ba2022-08-08 21:13:33 +010036 .setDescription("The number of minutes to mute the user for | Default: 0")
Skyler Grey75ea9172022-08-06 10:22:23 +010037 .setMinValue(0)
38 .setMaxValue(59)
39 .setRequired(false)
40 )
41 .addIntegerOption((option) =>
42 option
43 .setName("seconds")
Skyler Grey11236ba2022-08-08 21:13:33 +010044 .setDescription("The number of seconds to mute the user for | Default: 0")
Skyler Grey75ea9172022-08-06 10:22:23 +010045 .setMinValue(0)
46 .setMaxValue(59)
47 .setRequired(false)
48 );
pineafan8b4b17f2022-02-27 20:42:52 +000049
Skyler Greyc634e2b2022-08-06 17:50:48 +010050const callback = async (interaction: CommandInteraction): Promise<unknown> => {
Skyler Grey11236ba2022-08-08 21:13:33 +010051 const { log, NucleusColors, renderUser, entry, renderDelta } = client.logger;
pineafan63fc5e22022-08-04 22:04:10 +010052 const user = interaction.options.getMember("user") as GuildMember;
pineafan8b4b17f2022-02-27 20:42:52 +000053 const time = {
Skyler Greyc634e2b2022-08-06 17:50:48 +010054 days: interaction.options.getInteger("days") ?? 0,
55 hours: interaction.options.getInteger("hours") ?? 0,
56 minutes: interaction.options.getInteger("minutes") ?? 0,
57 seconds: interaction.options.getInteger("seconds") ?? 0
pineafan63fc5e22022-08-04 22:04:10 +010058 };
59 const config = await client.database.guilds.read(interaction.guild.id);
Skyler Grey11236ba2022-08-08 21:13:33 +010060 let serverSettingsDescription = config.moderation.mute.timeout ? "given a timeout" : "";
Skyler Grey75ea9172022-08-06 10:22:23 +010061 if (config.moderation.mute.role)
62 serverSettingsDescription +=
Skyler Grey11236ba2022-08-08 21:13:33 +010063 (serverSettingsDescription ? " and " : "") + `given the <@&${config.moderation.mute.role}> role`;
pineafane625d782022-05-09 18:04:32 +010064
Skyler Grey11236ba2022-08-08 21:13:33 +010065 let muteTime = time.days * 24 * 60 * 60 + time.hours * 60 * 60 + time.minutes * 60 + time.seconds;
pineafane23c4ec2022-07-27 21:56:27 +010066 if (muteTime === 0) {
Skyler Grey75ea9172022-08-06 10:22:23 +010067 const m = (await interaction.reply({
68 embeds: [
69 new EmojiEmbed()
70 .setEmoji("PUNISH.MUTE.GREEN")
71 .setTitle("Mute")
72 .setDescription("How long should the user be muted")
73 .setStatus("Success")
74 ],
75 components: [
76 new MessageActionRow().addComponents([
Skyler Grey11236ba2022-08-08 21:13:33 +010077 new Discord.MessageButton().setCustomId("1m").setLabel("1 Minute").setStyle("SECONDARY"),
78 new Discord.MessageButton().setCustomId("10m").setLabel("10 Minutes").setStyle("SECONDARY"),
79 new Discord.MessageButton().setCustomId("30m").setLabel("30 Minutes").setStyle("SECONDARY"),
80 new Discord.MessageButton().setCustomId("1h").setLabel("1 Hour").setStyle("SECONDARY")
Skyler Grey75ea9172022-08-06 10:22:23 +010081 ]),
82 new MessageActionRow().addComponents([
Skyler Grey11236ba2022-08-08 21:13:33 +010083 new Discord.MessageButton().setCustomId("6h").setLabel("6 Hours").setStyle("SECONDARY"),
84 new Discord.MessageButton().setCustomId("12h").setLabel("12 Hours").setStyle("SECONDARY"),
85 new Discord.MessageButton().setCustomId("1d").setLabel("1 Day").setStyle("SECONDARY"),
86 new Discord.MessageButton().setCustomId("1w").setLabel("1 Week").setStyle("SECONDARY")
Skyler Grey75ea9172022-08-06 10:22:23 +010087 ]),
88 new MessageActionRow().addComponents([
89 new Discord.MessageButton()
90 .setCustomId("cancel")
91 .setLabel("Cancel")
92 .setStyle("DANGER")
93 .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
94 ])
95 ],
96 ephemeral: true,
97 fetchReply: true
98 })) as Message;
pineafan8b4b17f2022-02-27 20:42:52 +000099 let component;
100 try {
Skyler Grey75ea9172022-08-06 10:22:23 +0100101 component = await m.awaitMessageComponent({
102 filter: (m) => m.user.id === interaction.user.id,
103 time: 300000
104 });
105 } catch {
106 return;
107 }
pineafan8b4b17f2022-02-27 20:42:52 +0000108 component.deferUpdate();
Skyler Grey75ea9172022-08-06 10:22:23 +0100109 if (component.customId === "cancel")
110 return interaction.editReply({
111 embeds: [
112 new EmojiEmbed()
113 .setEmoji("PUNISH.MUTE.RED")
114 .setTitle("Mute")
115 .setDescription("Mute cancelled")
116 .setStatus("Danger")
117 ]
118 });
pineafan8b4b17f2022-02-27 20:42:52 +0000119 switch (component.customId) {
Skyler Grey75ea9172022-08-06 10:22:23 +0100120 case "1m": {
121 muteTime = 60;
122 break;
123 }
124 case "10m": {
125 muteTime = 60 * 10;
126 break;
127 }
128 case "30m": {
129 muteTime = 60 * 30;
130 break;
131 }
132 case "1h": {
133 muteTime = 60 * 60;
134 break;
135 }
136 case "6h": {
137 muteTime = 60 * 60 * 6;
138 break;
139 }
140 case "12h": {
141 muteTime = 60 * 60 * 12;
142 break;
143 }
144 case "1d": {
145 muteTime = 60 * 60 * 24;
146 break;
147 }
148 case "1w": {
149 muteTime = 60 * 60 * 24 * 7;
150 break;
151 }
pineafan8b4b17f2022-02-27 20:42:52 +0000152 }
153 } else {
Skyler Grey75ea9172022-08-06 10:22:23 +0100154 await interaction.reply({
155 embeds: LoadingEmbed,
156 ephemeral: true,
157 fetchReply: true
158 });
pineafan8b4b17f2022-02-27 20:42:52 +0000159 }
pineafan5d1908e2022-02-28 21:34:47 +0000160 // TODO:[Modals] Replace this with a modal
pineafan73a7c4a2022-07-24 10:38:04 +0100161 let reason = null;
pineafan02ba0232022-07-24 22:16:15 +0100162 let notify = true;
163 let createAppealTicket = false;
pineafan73a7c4a2022-07-24 10:38:04 +0100164 let confirmation;
165 while (true) {
166 confirmation = await new confirmationMessage(interaction)
167 .setEmoji("PUNISH.MUTE.RED")
168 .setTitle("Mute")
Skyler Grey75ea9172022-08-06 10:22:23 +0100169 .setDescription(
170 keyValueList({
171 user: renderUser(user.user),
172 time: `${humanizeDuration(muteTime * 1000, {
173 round: true
174 })}`,
Skyler Grey11236ba2022-08-08 21:13:33 +0100175 reason: reason ? "\n> " + (reason ?? "").replaceAll("\n", "\n> ") : "*No reason provided*"
Skyler Grey75ea9172022-08-06 10:22:23 +0100176 }) +
177 "The user will be " +
178 serverSettingsDescription +
179 "\n" +
180 `The user **will${notify ? "" : " not"}** be notified\n\n` +
181 `Are you sure you want to mute <@!${user.id}>?`
182 )
pineafan73a7c4a2022-07-24 10:38:04 +0100183 .setColor("Danger")
184 .addCustomBoolean(
Skyler Grey75ea9172022-08-06 10:22:23 +0100185 "appeal",
186 "Create appeal ticket",
187 !(await areTicketsEnabled(interaction.guild.id)),
188 async () =>
Skyler Grey11236ba2022-08-08 21:13:33 +0100189 await create(interaction.guild, interaction.options.getUser("user"), interaction.user, reason),
Skyler Grey75ea9172022-08-06 10:22:23 +0100190 "An appeal ticket will be created when Confirm is clicked",
191 "CONTROL.TICKET",
192 createAppealTicket
193 )
194 .addCustomBoolean(
195 "notify",
196 "Notify user",
197 false,
198 null,
199 null,
200 "ICONS.NOTIFY." + (notify ? "ON" : "OFF"),
201 notify
202 )
pineafan73a7c4a2022-07-24 10:38:04 +0100203 .addReasonButton(reason ?? "")
pineafan63fc5e22022-08-04 22:04:10 +0100204 .send(true);
205 reason = reason ?? "";
206 if (confirmation.cancelled) return;
207 if (confirmation.success) break;
208 if (confirmation.newReason) reason = confirmation.newReason;
pineafan02ba0232022-07-24 22:16:15 +0100209 if (confirmation.components) {
pineafan63fc5e22022-08-04 22:04:10 +0100210 notify = confirmation.components.notify.active;
211 createAppealTicket = confirmation.components.appeal.active;
pineafan02ba0232022-07-24 22:16:15 +0100212 }
pineafan73a7c4a2022-07-24 10:38:04 +0100213 }
pineafan377794f2022-04-18 19:01:01 +0100214 if (confirmation.success) {
pineafan63fc5e22022-08-04 22:04:10 +0100215 let dmd = false;
pineafan5d1908e2022-02-28 21:34:47 +0000216 let dm;
pineafan63fc5e22022-08-04 22:04:10 +0100217 const config = await client.database.guilds.read(interaction.guild.id);
pineafan8b4b17f2022-02-27 20:42:52 +0000218 try {
pineafan02ba0232022-07-24 22:16:15 +0100219 if (notify) {
pineafan73a7c4a2022-07-24 10:38:04 +0100220 dm = await user.send({
Skyler Grey75ea9172022-08-06 10:22:23 +0100221 embeds: [
222 new EmojiEmbed()
223 .setEmoji("PUNISH.MUTE.RED")
224 .setTitle("Muted")
225 .setDescription(
226 `You have been muted in ${interaction.guild.name}` +
227 (reason
228 ? ` for:\n> ${reason}`
229 : ".\n\n" +
230 `You will be unmuted at: <t:${
Skyler Grey11236ba2022-08-08 21:13:33 +0100231 Math.round(new Date().getTime() / 1000) + muteTime
232 }:D> at <t:${Math.round(new Date().getTime() / 1000) + muteTime}:T> (<t:${
233 Math.round(new Date().getTime() / 1000) + muteTime
Skyler Grey75ea9172022-08-06 10:22:23 +0100234 }:R>)`) +
235 (confirmation.components.appeal.response
236 ? `You can appeal this here: <#${confirmation.components.appeal.response}>`
237 : "")
238 )
239 .setStatus("Danger")
pineafan377794f2022-04-18 19:01:01 +0100240 ],
Skyler Grey75ea9172022-08-06 10:22:23 +0100241 components: [
242 new MessageActionRow().addComponents(
243 config.moderation.mute.text
244 ? [
245 new MessageButton()
246 .setStyle("LINK")
247 .setLabel(config.moderation.mute.text)
248 .setURL(config.moderation.mute.link)
249 ]
250 : []
251 )
252 ]
pineafan63fc5e22022-08-04 22:04:10 +0100253 });
254 dmd = true;
pineafan8b4b17f2022-02-27 20:42:52 +0000255 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100256 } catch {
257 dmd = false;
258 }
pineafan63fc5e22022-08-04 22:04:10 +0100259 const member = user;
260 let errors = 0;
pineafan8b4b17f2022-02-27 20:42:52 +0000261 try {
pineafane625d782022-05-09 18:04:32 +0100262 if (config.moderation.mute.timeout) {
Skyler Grey11236ba2022-08-08 21:13:33 +0100263 await member.timeout(muteTime * 1000, reason || "No reason provided");
pineafan73a7c4a2022-07-24 10:38:04 +0100264 if (config.moderation.mute.role !== null) {
pineafan63fc5e22022-08-04 22:04:10 +0100265 await member.roles.add(config.moderation.mute.role);
Skyler Grey75ea9172022-08-06 10:22:23 +0100266 await client.database.eventScheduler.schedule(
267 "naturalUnmute",
268 new Date().getTime() + muteTime * 1000,
269 {
270 guild: interaction.guild.id,
271 user: user.id,
272 expires: new Date().getTime() + muteTime * 1000
273 }
274 );
pineafan73a7c4a2022-07-24 10:38:04 +0100275 }
pineafane625d782022-05-09 18:04:32 +0100276 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100277 } catch {
278 errors++;
279 }
pineafan73a7c4a2022-07-24 10:38:04 +0100280 try {
281 if (config.moderation.mute.role !== null) {
pineafan63fc5e22022-08-04 22:04:10 +0100282 await member.roles.add(config.moderation.mute.role);
Skyler Grey11236ba2022-08-08 21:13:33 +0100283 await client.database.eventScheduler.schedule("unmuteRole", new Date().getTime() + muteTime * 1000, {
284 guild: interaction.guild.id,
285 user: user.id,
286 role: config.moderation.mute.role
287 });
pineafan73a7c4a2022-07-24 10:38:04 +0100288 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100289 } catch (e) {
290 console.log(e);
291 errors++;
292 }
pineafane23c4ec2022-07-27 21:56:27 +0100293 if (errors === 2) {
Skyler Grey75ea9172022-08-06 10:22:23 +0100294 await interaction.editReply({
295 embeds: [
296 new EmojiEmbed()
297 .setEmoji("PUNISH.MUTE.RED")
298 .setTitle("Mute")
Skyler Grey11236ba2022-08-08 21:13:33 +0100299 .setDescription("Something went wrong and the user was not muted")
Skyler Grey75ea9172022-08-06 10:22:23 +0100300 .setStatus("Danger")
301 ],
302 components: []
303 }); // TODO: make this clearer
pineafan63fc5e22022-08-04 22:04:10 +0100304 if (dmd) await dm.delete();
305 return;
pineafan8b4b17f2022-02-27 20:42:52 +0000306 }
Skyler Grey11236ba2022-08-08 21:13:33 +0100307 await client.database.history.create("mute", interaction.guild.id, member.user, interaction.user, reason);
Skyler Grey75ea9172022-08-06 10:22:23 +0100308 const failed = !dmd && notify;
309 await interaction.editReply({
310 embeds: [
311 new EmojiEmbed()
312 .setEmoji(`PUNISH.MUTE.${failed ? "YELLOW" : "GREEN"}`)
313 .setTitle("Mute")
314 .setDescription(
315 "The member was muted" +
316 (failed ? ", but could not be notified" : "") +
317 (confirmation.components.appeal.response
318 ? ` and an appeal ticket was opened in <#${confirmation.components.appeal.response}>`
319 : "")
320 )
321 .setStatus(failed ? "Warning" : "Success")
322 ],
323 components: []
324 });
pineafan63fc5e22022-08-04 22:04:10 +0100325 const data = {
Skyler Grey75ea9172022-08-06 10:22:23 +0100326 meta: {
pineafan63fc5e22022-08-04 22:04:10 +0100327 type: "memberMute",
328 displayName: "Member Muted",
329 calculateType: "guildMemberPunish",
pineafan377794f2022-04-18 19:01:01 +0100330 color: NucleusColors.yellow,
pineafan63fc5e22022-08-04 22:04:10 +0100331 emoji: "PUNISH.WARN.YELLOW",
pineafan377794f2022-04-18 19:01:01 +0100332 timestamp: new Date().getTime()
333 },
334 list: {
pineafan73a7c4a2022-07-24 10:38:04 +0100335 memberId: entry(member.user.id, `\`${member.user.id}\``),
336 name: entry(member.user.id, renderUser(member.user)),
Skyler Grey75ea9172022-08-06 10:22:23 +0100337 mutedUntil: entry(
338 new Date().getTime() + muteTime * 1000,
339 renderDelta(new Date().getTime() + muteTime * 1000)
340 ),
Skyler Grey11236ba2022-08-08 21:13:33 +0100341 muted: entry(new Date().getTime(), renderDelta(new Date().getTime() - 1000)),
342 mutedBy: entry(interaction.member.user.id, renderUser(interaction.member.user)),
pineafan63fc5e22022-08-04 22:04:10 +0100343 reason: entry(reason, reason ? reason : "*No reason provided*")
pineafan377794f2022-04-18 19:01:01 +0100344 },
345 hidden: {
346 guild: interaction.guild.id
347 }
pineafan63fc5e22022-08-04 22:04:10 +0100348 };
pineafan4edb7762022-06-26 19:21:04 +0100349 log(data);
pineafan8b4b17f2022-02-27 20:42:52 +0000350 } else {
Skyler Grey75ea9172022-08-06 10:22:23 +0100351 await interaction.editReply({
352 embeds: [
353 new EmojiEmbed()
354 .setEmoji("PUNISH.MUTE.GREEN")
355 .setTitle("Mute")
356 .setDescription("No changes were made")
357 .setStatus("Success")
358 ],
359 components: []
360 });
pineafan8b4b17f2022-02-27 20:42:52 +0000361 }
pineafan63fc5e22022-08-04 22:04:10 +0100362};
pineafan8b4b17f2022-02-27 20:42:52 +0000363
pineafanbd02b4a2022-08-05 22:01:38 +0100364const check = (interaction: CommandInteraction) => {
Skyler Grey75ea9172022-08-06 10:22:23 +0100365 const member = interaction.member as GuildMember;
366 const me = interaction.guild.me!;
367 const apply = interaction.options.getMember("user") as GuildMember;
pineafan3a02ea32022-08-11 21:35:04 +0100368 if (member === null || me === null || apply === null) throw new Error("That member is not in the server");
pineafan63fc5e22022-08-04 22:04:10 +0100369 const memberPos = member.roles ? member.roles.highest.position : 0;
370 const mePos = me.roles ? me.roles.highest.position : 0;
371 const applyPos = apply.roles ? apply.roles.highest.position : 0;
pineafanc1c18792022-08-03 21:41:36 +0100372 // Do not allow muting the owner
pineafan3a02ea32022-08-11 21:35:04 +0100373 if (member.id === interaction.guild.ownerId) throw new Error("You cannot mute the owner of the server");
pineafan8b4b17f2022-02-27 20:42:52 +0000374 // Check if Nucleus can mute the member
pineafan3a02ea32022-08-11 21:35:04 +0100375 if (!(mePos > applyPos)) throw new Error("I do not have a role higher than that member");
pineafan8b4b17f2022-02-27 20:42:52 +0000376 // Check if Nucleus has permission to mute
pineafan3a02ea32022-08-11 21:35:04 +0100377 if (!me.permissions.has("MODERATE_MEMBERS")) throw new Error("I do not have the *Moderate Members* permission");
pineafan8b4b17f2022-02-27 20:42:52 +0000378 // Do not allow muting Nucleus
pineafan3a02ea32022-08-11 21:35:04 +0100379 if (member.id === me.id) throw new Error("I cannot mute myself");
pineafan8b4b17f2022-02-27 20:42:52 +0000380 // Allow the owner to mute anyone
pineafan63fc5e22022-08-04 22:04:10 +0100381 if (member.id === interaction.guild.ownerId) return true;
pineafan8b4b17f2022-02-27 20:42:52 +0000382 // Check if the user has moderate_members permission
pineafan3a02ea32022-08-11 21:35:04 +0100383 if (!member.permissions.has("MODERATE_MEMBERS"))
384 throw new Error("You do not have the *Moderate Members* permission");
pineafan8b4b17f2022-02-27 20:42:52 +0000385 // Check if the user is below on the role list
pineafan3a02ea32022-08-11 21:35:04 +0100386 if (!(memberPos > applyPos)) throw new Error("You do not have a role higher than that member");
pineafan8b4b17f2022-02-27 20:42:52 +0000387 // Allow mute
pineafan63fc5e22022-08-04 22:04:10 +0100388 return true;
389};
pineafan8b4b17f2022-02-27 20:42:52 +0000390
Skyler Grey75ea9172022-08-06 10:22:23 +0100391export { command, callback, check };