blob: 5be6f436bd8b0ffa685ef1153af5ec8bb29f398f [file] [log] [blame]
pineafan63fc5e22022-08-04 22:04:10 +01001import { LoadingEmbed } from "./../../utils/defaultEmbeds.js";
Skyler Grey75ea9172022-08-06 10:22:23 +01002import Discord, {
3 CommandInteraction,
4 GuildMember,
5 Message,
6 MessageActionRow,
7 MessageButton
8} from "discord.js";
pineafan8b4b17f2022-02-27 20:42:52 +00009import { SlashCommandSubcommandBuilder } from "@discordjs/builders";
pineafan4edb7762022-06-26 19:21:04 +010010import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
pineafan8b4b17f2022-02-27 20:42:52 +000011import getEmojiByName from "../../utils/getEmojiByName.js";
12import confirmationMessage from "../../utils/confirmationMessage.js";
13import keyValueList from "../../utils/generateKeyValueList.js";
14import humanizeDuration from "humanize-duration";
pineafan6702cef2022-06-13 17:52:37 +010015import client from "../../utils/client.js";
Skyler Grey75ea9172022-08-06 10:22:23 +010016import {
17 areTicketsEnabled,
18 create
19} from "../../actions/createModActionTicket.js";
pineafan8b4b17f2022-02-27 20:42:52 +000020
21const command = (builder: SlashCommandSubcommandBuilder) =>
22 builder
pineafan63fc5e22022-08-04 22:04:10 +010023 .setName("mute")
Skyler Grey75ea9172022-08-06 10:22:23 +010024 .setDescription(
25 "Mutes a member, stopping them from talking in the server"
26 )
27 .addUserOption((option) =>
28 option
29 .setName("user")
30 .setDescription("The user to mute")
31 .setRequired(true)
32 )
33 .addIntegerOption((option) =>
34 option
35 .setName("days")
36 .setDescription(
37 "The number of days to mute the user for | Default: 0"
38 )
39 .setMinValue(0)
40 .setMaxValue(27)
41 .setRequired(false)
42 )
43 .addIntegerOption((option) =>
44 option
45 .setName("hours")
46 .setDescription(
47 "The number of hours to mute the user for | Default: 0"
48 )
49 .setMinValue(0)
50 .setMaxValue(23)
51 .setRequired(false)
52 )
53 .addIntegerOption((option) =>
54 option
55 .setName("minutes")
56 .setDescription(
57 "The number of minutes to mute the user for | Default: 0"
58 )
59 .setMinValue(0)
60 .setMaxValue(59)
61 .setRequired(false)
62 )
63 .addIntegerOption((option) =>
64 option
65 .setName("seconds")
66 .setDescription(
67 "The number of seconds to mute the user for | Default: 0"
68 )
69 .setMinValue(0)
70 .setMaxValue(59)
71 .setRequired(false)
72 );
pineafan8b4b17f2022-02-27 20:42:52 +000073
Skyler Grey75ea9172022-08-06 10:22:23 +010074const callback = async (
75 interaction: CommandInteraction
76): Promise<void | unknown> => {
77 const { log, NucleusColors, renderUser, entry, renderDelta } =
78 client.logger;
pineafan63fc5e22022-08-04 22:04:10 +010079 const user = interaction.options.getMember("user") as GuildMember;
pineafan8b4b17f2022-02-27 20:42:52 +000080 const time = {
81 days: interaction.options.getInteger("days") || 0,
82 hours: interaction.options.getInteger("hours") || 0,
83 minutes: interaction.options.getInteger("minutes") || 0,
84 seconds: interaction.options.getInteger("seconds") || 0
pineafan63fc5e22022-08-04 22:04:10 +010085 };
86 const config = await client.database.guilds.read(interaction.guild.id);
Skyler Grey75ea9172022-08-06 10:22:23 +010087 let serverSettingsDescription = config.moderation.mute.timeout
88 ? "given a timeout"
89 : "";
90 if (config.moderation.mute.role)
91 serverSettingsDescription +=
92 (serverSettingsDescription ? " and " : "") +
93 `given the <@&${config.moderation.mute.role}> role`;
pineafane625d782022-05-09 18:04:32 +010094
Skyler Grey75ea9172022-08-06 10:22:23 +010095 let muteTime =
96 time.days * 24 * 60 * 60 +
97 time.hours * 60 * 60 +
98 time.minutes * 60 +
99 time.seconds;
pineafane23c4ec2022-07-27 21:56:27 +0100100 if (muteTime === 0) {
Skyler Grey75ea9172022-08-06 10:22:23 +0100101 const m = (await interaction.reply({
102 embeds: [
103 new EmojiEmbed()
104 .setEmoji("PUNISH.MUTE.GREEN")
105 .setTitle("Mute")
106 .setDescription("How long should the user be muted")
107 .setStatus("Success")
108 ],
109 components: [
110 new MessageActionRow().addComponents([
111 new Discord.MessageButton()
112 .setCustomId("1m")
113 .setLabel("1 Minute")
114 .setStyle("SECONDARY"),
115 new Discord.MessageButton()
116 .setCustomId("10m")
117 .setLabel("10 Minutes")
118 .setStyle("SECONDARY"),
119 new Discord.MessageButton()
120 .setCustomId("30m")
121 .setLabel("30 Minutes")
122 .setStyle("SECONDARY"),
123 new Discord.MessageButton()
124 .setCustomId("1h")
125 .setLabel("1 Hour")
126 .setStyle("SECONDARY")
127 ]),
128 new MessageActionRow().addComponents([
129 new Discord.MessageButton()
130 .setCustomId("6h")
131 .setLabel("6 Hours")
132 .setStyle("SECONDARY"),
133 new Discord.MessageButton()
134 .setCustomId("12h")
135 .setLabel("12 Hours")
136 .setStyle("SECONDARY"),
137 new Discord.MessageButton()
138 .setCustomId("1d")
139 .setLabel("1 Day")
140 .setStyle("SECONDARY"),
141 new Discord.MessageButton()
142 .setCustomId("1w")
143 .setLabel("1 Week")
144 .setStyle("SECONDARY")
145 ]),
146 new MessageActionRow().addComponents([
147 new Discord.MessageButton()
148 .setCustomId("cancel")
149 .setLabel("Cancel")
150 .setStyle("DANGER")
151 .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
152 ])
153 ],
154 ephemeral: true,
155 fetchReply: true
156 })) as Message;
pineafan8b4b17f2022-02-27 20:42:52 +0000157 let component;
158 try {
Skyler Grey75ea9172022-08-06 10:22:23 +0100159 component = await m.awaitMessageComponent({
160 filter: (m) => m.user.id === interaction.user.id,
161 time: 300000
162 });
163 } catch {
164 return;
165 }
pineafan8b4b17f2022-02-27 20:42:52 +0000166 component.deferUpdate();
Skyler Grey75ea9172022-08-06 10:22:23 +0100167 if (component.customId === "cancel")
168 return interaction.editReply({
169 embeds: [
170 new EmojiEmbed()
171 .setEmoji("PUNISH.MUTE.RED")
172 .setTitle("Mute")
173 .setDescription("Mute cancelled")
174 .setStatus("Danger")
175 ]
176 });
pineafan8b4b17f2022-02-27 20:42:52 +0000177 switch (component.customId) {
Skyler Grey75ea9172022-08-06 10:22:23 +0100178 case "1m": {
179 muteTime = 60;
180 break;
181 }
182 case "10m": {
183 muteTime = 60 * 10;
184 break;
185 }
186 case "30m": {
187 muteTime = 60 * 30;
188 break;
189 }
190 case "1h": {
191 muteTime = 60 * 60;
192 break;
193 }
194 case "6h": {
195 muteTime = 60 * 60 * 6;
196 break;
197 }
198 case "12h": {
199 muteTime = 60 * 60 * 12;
200 break;
201 }
202 case "1d": {
203 muteTime = 60 * 60 * 24;
204 break;
205 }
206 case "1w": {
207 muteTime = 60 * 60 * 24 * 7;
208 break;
209 }
pineafan8b4b17f2022-02-27 20:42:52 +0000210 }
211 } else {
Skyler Grey75ea9172022-08-06 10:22:23 +0100212 await interaction.reply({
213 embeds: LoadingEmbed,
214 ephemeral: true,
215 fetchReply: true
216 });
pineafan8b4b17f2022-02-27 20:42:52 +0000217 }
pineafan5d1908e2022-02-28 21:34:47 +0000218 // TODO:[Modals] Replace this with a modal
pineafan73a7c4a2022-07-24 10:38:04 +0100219 let reason = null;
pineafan02ba0232022-07-24 22:16:15 +0100220 let notify = true;
221 let createAppealTicket = false;
pineafan73a7c4a2022-07-24 10:38:04 +0100222 let confirmation;
223 while (true) {
224 confirmation = await new confirmationMessage(interaction)
225 .setEmoji("PUNISH.MUTE.RED")
226 .setTitle("Mute")
Skyler Grey75ea9172022-08-06 10:22:23 +0100227 .setDescription(
228 keyValueList({
229 user: renderUser(user.user),
230 time: `${humanizeDuration(muteTime * 1000, {
231 round: true
232 })}`,
233 reason: reason
234 ? "\n> " + (reason ?? "").replaceAll("\n", "\n> ")
235 : "*No reason provided*"
236 }) +
237 "The user will be " +
238 serverSettingsDescription +
239 "\n" +
240 `The user **will${notify ? "" : " not"}** be notified\n\n` +
241 `Are you sure you want to mute <@!${user.id}>?`
242 )
pineafan73a7c4a2022-07-24 10:38:04 +0100243 .setColor("Danger")
244 .addCustomBoolean(
Skyler Grey75ea9172022-08-06 10:22:23 +0100245 "appeal",
246 "Create appeal ticket",
247 !(await areTicketsEnabled(interaction.guild.id)),
248 async () =>
249 await create(
250 interaction.guild,
251 interaction.options.getUser("user"),
252 interaction.user,
253 reason
254 ),
255 "An appeal ticket will be created when Confirm is clicked",
256 "CONTROL.TICKET",
257 createAppealTicket
258 )
259 .addCustomBoolean(
260 "notify",
261 "Notify user",
262 false,
263 null,
264 null,
265 "ICONS.NOTIFY." + (notify ? "ON" : "OFF"),
266 notify
267 )
pineafan73a7c4a2022-07-24 10:38:04 +0100268 .addReasonButton(reason ?? "")
pineafan63fc5e22022-08-04 22:04:10 +0100269 .send(true);
270 reason = reason ?? "";
271 if (confirmation.cancelled) return;
272 if (confirmation.success) break;
273 if (confirmation.newReason) reason = confirmation.newReason;
pineafan02ba0232022-07-24 22:16:15 +0100274 if (confirmation.components) {
pineafan63fc5e22022-08-04 22:04:10 +0100275 notify = confirmation.components.notify.active;
276 createAppealTicket = confirmation.components.appeal.active;
pineafan02ba0232022-07-24 22:16:15 +0100277 }
pineafan73a7c4a2022-07-24 10:38:04 +0100278 }
pineafan377794f2022-04-18 19:01:01 +0100279 if (confirmation.success) {
pineafan63fc5e22022-08-04 22:04:10 +0100280 let dmd = false;
pineafan5d1908e2022-02-28 21:34:47 +0000281 let dm;
pineafan63fc5e22022-08-04 22:04:10 +0100282 const config = await client.database.guilds.read(interaction.guild.id);
pineafan8b4b17f2022-02-27 20:42:52 +0000283 try {
pineafan02ba0232022-07-24 22:16:15 +0100284 if (notify) {
pineafan73a7c4a2022-07-24 10:38:04 +0100285 dm = await user.send({
Skyler Grey75ea9172022-08-06 10:22:23 +0100286 embeds: [
287 new EmojiEmbed()
288 .setEmoji("PUNISH.MUTE.RED")
289 .setTitle("Muted")
290 .setDescription(
291 `You have been muted in ${interaction.guild.name}` +
292 (reason
293 ? ` for:\n> ${reason}`
294 : ".\n\n" +
295 `You will be unmuted at: <t:${
296 Math.round(
297 new Date().getTime() / 1000
298 ) + muteTime
299 }:D> at <t:${
300 Math.round(
301 new Date().getTime() / 1000
302 ) + muteTime
303 }:T> (<t:${
304 Math.round(
305 new Date().getTime() / 1000
306 ) + muteTime
307 }:R>)`) +
308 (confirmation.components.appeal.response
309 ? `You can appeal this here: <#${confirmation.components.appeal.response}>`
310 : "")
311 )
312 .setStatus("Danger")
pineafan377794f2022-04-18 19:01:01 +0100313 ],
Skyler Grey75ea9172022-08-06 10:22:23 +0100314 components: [
315 new MessageActionRow().addComponents(
316 config.moderation.mute.text
317 ? [
318 new MessageButton()
319 .setStyle("LINK")
320 .setLabel(config.moderation.mute.text)
321 .setURL(config.moderation.mute.link)
322 ]
323 : []
324 )
325 ]
pineafan63fc5e22022-08-04 22:04:10 +0100326 });
327 dmd = true;
pineafan8b4b17f2022-02-27 20:42:52 +0000328 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100329 } catch {
330 dmd = false;
331 }
pineafan63fc5e22022-08-04 22:04:10 +0100332 const member = user;
333 let errors = 0;
pineafan8b4b17f2022-02-27 20:42:52 +0000334 try {
pineafane625d782022-05-09 18:04:32 +0100335 if (config.moderation.mute.timeout) {
Skyler Grey75ea9172022-08-06 10:22:23 +0100336 await member.timeout(
337 muteTime * 1000,
338 reason || "No reason provided"
339 );
pineafan73a7c4a2022-07-24 10:38:04 +0100340 if (config.moderation.mute.role !== null) {
pineafan63fc5e22022-08-04 22:04:10 +0100341 await member.roles.add(config.moderation.mute.role);
Skyler Grey75ea9172022-08-06 10:22:23 +0100342 await client.database.eventScheduler.schedule(
343 "naturalUnmute",
344 new Date().getTime() + muteTime * 1000,
345 {
346 guild: interaction.guild.id,
347 user: user.id,
348 expires: new Date().getTime() + muteTime * 1000
349 }
350 );
pineafan73a7c4a2022-07-24 10:38:04 +0100351 }
pineafane625d782022-05-09 18:04:32 +0100352 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100353 } catch {
354 errors++;
355 }
pineafan73a7c4a2022-07-24 10:38:04 +0100356 try {
357 if (config.moderation.mute.role !== null) {
pineafan63fc5e22022-08-04 22:04:10 +0100358 await member.roles.add(config.moderation.mute.role);
Skyler Grey75ea9172022-08-06 10:22:23 +0100359 await client.database.eventScheduler.schedule(
360 "unmuteRole",
361 new Date().getTime() + muteTime * 1000,
362 {
363 guild: interaction.guild.id,
364 user: user.id,
365 role: config.moderation.mute.role
366 }
367 );
pineafan73a7c4a2022-07-24 10:38:04 +0100368 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100369 } catch (e) {
370 console.log(e);
371 errors++;
372 }
pineafane23c4ec2022-07-27 21:56:27 +0100373 if (errors === 2) {
Skyler Grey75ea9172022-08-06 10:22:23 +0100374 await interaction.editReply({
375 embeds: [
376 new EmojiEmbed()
377 .setEmoji("PUNISH.MUTE.RED")
378 .setTitle("Mute")
379 .setDescription(
380 "Something went wrong and the user was not muted"
381 )
382 .setStatus("Danger")
383 ],
384 components: []
385 }); // TODO: make this clearer
pineafan63fc5e22022-08-04 22:04:10 +0100386 if (dmd) await dm.delete();
387 return;
pineafan8b4b17f2022-02-27 20:42:52 +0000388 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100389 await client.database.history.create(
390 "mute",
391 interaction.guild.id,
392 member.user,
393 interaction.user,
394 reason
395 );
396 const failed = !dmd && notify;
397 await interaction.editReply({
398 embeds: [
399 new EmojiEmbed()
400 .setEmoji(`PUNISH.MUTE.${failed ? "YELLOW" : "GREEN"}`)
401 .setTitle("Mute")
402 .setDescription(
403 "The member was muted" +
404 (failed ? ", but could not be notified" : "") +
405 (confirmation.components.appeal.response
406 ? ` and an appeal ticket was opened in <#${confirmation.components.appeal.response}>`
407 : "")
408 )
409 .setStatus(failed ? "Warning" : "Success")
410 ],
411 components: []
412 });
pineafan63fc5e22022-08-04 22:04:10 +0100413 const data = {
Skyler Grey75ea9172022-08-06 10:22:23 +0100414 meta: {
pineafan63fc5e22022-08-04 22:04:10 +0100415 type: "memberMute",
416 displayName: "Member Muted",
417 calculateType: "guildMemberPunish",
pineafan377794f2022-04-18 19:01:01 +0100418 color: NucleusColors.yellow,
pineafan63fc5e22022-08-04 22:04:10 +0100419 emoji: "PUNISH.WARN.YELLOW",
pineafan377794f2022-04-18 19:01:01 +0100420 timestamp: new Date().getTime()
421 },
422 list: {
pineafan73a7c4a2022-07-24 10:38:04 +0100423 memberId: entry(member.user.id, `\`${member.user.id}\``),
424 name: entry(member.user.id, renderUser(member.user)),
Skyler Grey75ea9172022-08-06 10:22:23 +0100425 mutedUntil: entry(
426 new Date().getTime() + muteTime * 1000,
427 renderDelta(new Date().getTime() + muteTime * 1000)
428 ),
429 muted: entry(
430 new Date().getTime(),
431 renderDelta(new Date().getTime() - 1000)
432 ),
433 mutedBy: entry(
434 interaction.member.user.id,
435 renderUser(interaction.member.user)
436 ),
pineafan63fc5e22022-08-04 22:04:10 +0100437 reason: entry(reason, reason ? reason : "*No reason provided*")
pineafan377794f2022-04-18 19:01:01 +0100438 },
439 hidden: {
440 guild: interaction.guild.id
441 }
pineafan63fc5e22022-08-04 22:04:10 +0100442 };
pineafan4edb7762022-06-26 19:21:04 +0100443 log(data);
pineafan8b4b17f2022-02-27 20:42:52 +0000444 } else {
Skyler Grey75ea9172022-08-06 10:22:23 +0100445 await interaction.editReply({
446 embeds: [
447 new EmojiEmbed()
448 .setEmoji("PUNISH.MUTE.GREEN")
449 .setTitle("Mute")
450 .setDescription("No changes were made")
451 .setStatus("Success")
452 ],
453 components: []
454 });
pineafan8b4b17f2022-02-27 20:42:52 +0000455 }
pineafan63fc5e22022-08-04 22:04:10 +0100456};
pineafan8b4b17f2022-02-27 20:42:52 +0000457
pineafanbd02b4a2022-08-05 22:01:38 +0100458const check = (interaction: CommandInteraction) => {
Skyler Grey75ea9172022-08-06 10:22:23 +0100459 const member = interaction.member as GuildMember;
460 const me = interaction.guild.me!;
461 const apply = interaction.options.getMember("user") as GuildMember;
462 if (member === null || me === null || apply === null)
463 throw "That member is not in the server";
pineafan63fc5e22022-08-04 22:04:10 +0100464 const memberPos = member.roles ? member.roles.highest.position : 0;
465 const mePos = me.roles ? me.roles.highest.position : 0;
466 const applyPos = apply.roles ? apply.roles.highest.position : 0;
pineafanc1c18792022-08-03 21:41:36 +0100467 // Do not allow muting the owner
Skyler Grey75ea9172022-08-06 10:22:23 +0100468 if (member.id === interaction.guild.ownerId)
469 throw "You cannot mute the owner of the server";
pineafan8b4b17f2022-02-27 20:42:52 +0000470 // Check if Nucleus can mute the member
Skyler Grey75ea9172022-08-06 10:22:23 +0100471 if (!(mePos > applyPos))
472 throw "I do not have a role higher than that member";
pineafan8b4b17f2022-02-27 20:42:52 +0000473 // Check if Nucleus has permission to mute
Skyler Grey75ea9172022-08-06 10:22:23 +0100474 if (!me.permissions.has("MODERATE_MEMBERS"))
475 throw "I do not have the *Moderate Members* permission";
pineafan8b4b17f2022-02-27 20:42:52 +0000476 // Do not allow muting Nucleus
pineafan63fc5e22022-08-04 22:04:10 +0100477 if (member.id === me.id) throw "I cannot mute myself";
pineafan8b4b17f2022-02-27 20:42:52 +0000478 // Allow the owner to mute anyone
pineafan63fc5e22022-08-04 22:04:10 +0100479 if (member.id === interaction.guild.ownerId) return true;
pineafan8b4b17f2022-02-27 20:42:52 +0000480 // Check if the user has moderate_members permission
Skyler Grey75ea9172022-08-06 10:22:23 +0100481 if (!member.permissions.has("MODERATE_MEMBERS"))
482 throw "You do not have the *Moderate Members* permission";
pineafan8b4b17f2022-02-27 20:42:52 +0000483 // Check if the user is below on the role list
Skyler Grey75ea9172022-08-06 10:22:23 +0100484 if (!(memberPos > applyPos))
485 throw "You do not have a role higher than that member";
pineafan8b4b17f2022-02-27 20:42:52 +0000486 // Allow mute
pineafan63fc5e22022-08-04 22:04:10 +0100487 return true;
488};
pineafan8b4b17f2022-02-27 20:42:52 +0000489
Skyler Grey75ea9172022-08-06 10:22:23 +0100490export { command, callback, check };