blob: b4301912b632fe0bacfda415877de897d9419c72 [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 Greyc634e2b2022-08-06 17:50:48 +010074const callback = async (interaction: CommandInteraction): Promise<unknown> => {
Skyler Grey75ea9172022-08-06 10:22:23 +010075 const { log, NucleusColors, renderUser, entry, renderDelta } =
76 client.logger;
pineafan63fc5e22022-08-04 22:04:10 +010077 const user = interaction.options.getMember("user") as GuildMember;
pineafan8b4b17f2022-02-27 20:42:52 +000078 const time = {
Skyler Greyc634e2b2022-08-06 17:50:48 +010079 days: interaction.options.getInteger("days") ?? 0,
80 hours: interaction.options.getInteger("hours") ?? 0,
81 minutes: interaction.options.getInteger("minutes") ?? 0,
82 seconds: interaction.options.getInteger("seconds") ?? 0
pineafan63fc5e22022-08-04 22:04:10 +010083 };
84 const config = await client.database.guilds.read(interaction.guild.id);
Skyler Grey75ea9172022-08-06 10:22:23 +010085 let serverSettingsDescription = config.moderation.mute.timeout
86 ? "given a timeout"
87 : "";
88 if (config.moderation.mute.role)
89 serverSettingsDescription +=
90 (serverSettingsDescription ? " and " : "") +
91 `given the <@&${config.moderation.mute.role}> role`;
pineafane625d782022-05-09 18:04:32 +010092
Skyler Grey75ea9172022-08-06 10:22:23 +010093 let muteTime =
94 time.days * 24 * 60 * 60 +
95 time.hours * 60 * 60 +
96 time.minutes * 60 +
97 time.seconds;
pineafane23c4ec2022-07-27 21:56:27 +010098 if (muteTime === 0) {
Skyler Grey75ea9172022-08-06 10:22:23 +010099 const m = (await interaction.reply({
100 embeds: [
101 new EmojiEmbed()
102 .setEmoji("PUNISH.MUTE.GREEN")
103 .setTitle("Mute")
104 .setDescription("How long should the user be muted")
105 .setStatus("Success")
106 ],
107 components: [
108 new MessageActionRow().addComponents([
109 new Discord.MessageButton()
110 .setCustomId("1m")
111 .setLabel("1 Minute")
112 .setStyle("SECONDARY"),
113 new Discord.MessageButton()
114 .setCustomId("10m")
115 .setLabel("10 Minutes")
116 .setStyle("SECONDARY"),
117 new Discord.MessageButton()
118 .setCustomId("30m")
119 .setLabel("30 Minutes")
120 .setStyle("SECONDARY"),
121 new Discord.MessageButton()
122 .setCustomId("1h")
123 .setLabel("1 Hour")
124 .setStyle("SECONDARY")
125 ]),
126 new MessageActionRow().addComponents([
127 new Discord.MessageButton()
128 .setCustomId("6h")
129 .setLabel("6 Hours")
130 .setStyle("SECONDARY"),
131 new Discord.MessageButton()
132 .setCustomId("12h")
133 .setLabel("12 Hours")
134 .setStyle("SECONDARY"),
135 new Discord.MessageButton()
136 .setCustomId("1d")
137 .setLabel("1 Day")
138 .setStyle("SECONDARY"),
139 new Discord.MessageButton()
140 .setCustomId("1w")
141 .setLabel("1 Week")
142 .setStyle("SECONDARY")
143 ]),
144 new MessageActionRow().addComponents([
145 new Discord.MessageButton()
146 .setCustomId("cancel")
147 .setLabel("Cancel")
148 .setStyle("DANGER")
149 .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
150 ])
151 ],
152 ephemeral: true,
153 fetchReply: true
154 })) as Message;
pineafan8b4b17f2022-02-27 20:42:52 +0000155 let component;
156 try {
Skyler Grey75ea9172022-08-06 10:22:23 +0100157 component = await m.awaitMessageComponent({
158 filter: (m) => m.user.id === interaction.user.id,
159 time: 300000
160 });
161 } catch {
162 return;
163 }
pineafan8b4b17f2022-02-27 20:42:52 +0000164 component.deferUpdate();
Skyler Grey75ea9172022-08-06 10:22:23 +0100165 if (component.customId === "cancel")
166 return interaction.editReply({
167 embeds: [
168 new EmojiEmbed()
169 .setEmoji("PUNISH.MUTE.RED")
170 .setTitle("Mute")
171 .setDescription("Mute cancelled")
172 .setStatus("Danger")
173 ]
174 });
pineafan8b4b17f2022-02-27 20:42:52 +0000175 switch (component.customId) {
Skyler Grey75ea9172022-08-06 10:22:23 +0100176 case "1m": {
177 muteTime = 60;
178 break;
179 }
180 case "10m": {
181 muteTime = 60 * 10;
182 break;
183 }
184 case "30m": {
185 muteTime = 60 * 30;
186 break;
187 }
188 case "1h": {
189 muteTime = 60 * 60;
190 break;
191 }
192 case "6h": {
193 muteTime = 60 * 60 * 6;
194 break;
195 }
196 case "12h": {
197 muteTime = 60 * 60 * 12;
198 break;
199 }
200 case "1d": {
201 muteTime = 60 * 60 * 24;
202 break;
203 }
204 case "1w": {
205 muteTime = 60 * 60 * 24 * 7;
206 break;
207 }
pineafan8b4b17f2022-02-27 20:42:52 +0000208 }
209 } else {
Skyler Grey75ea9172022-08-06 10:22:23 +0100210 await interaction.reply({
211 embeds: LoadingEmbed,
212 ephemeral: true,
213 fetchReply: true
214 });
pineafan8b4b17f2022-02-27 20:42:52 +0000215 }
pineafan5d1908e2022-02-28 21:34:47 +0000216 // TODO:[Modals] Replace this with a modal
pineafan73a7c4a2022-07-24 10:38:04 +0100217 let reason = null;
pineafan02ba0232022-07-24 22:16:15 +0100218 let notify = true;
219 let createAppealTicket = false;
pineafan73a7c4a2022-07-24 10:38:04 +0100220 let confirmation;
221 while (true) {
222 confirmation = await new confirmationMessage(interaction)
223 .setEmoji("PUNISH.MUTE.RED")
224 .setTitle("Mute")
Skyler Grey75ea9172022-08-06 10:22:23 +0100225 .setDescription(
226 keyValueList({
227 user: renderUser(user.user),
228 time: `${humanizeDuration(muteTime * 1000, {
229 round: true
230 })}`,
231 reason: reason
232 ? "\n> " + (reason ?? "").replaceAll("\n", "\n> ")
233 : "*No reason provided*"
234 }) +
235 "The user will be " +
236 serverSettingsDescription +
237 "\n" +
238 `The user **will${notify ? "" : " not"}** be notified\n\n` +
239 `Are you sure you want to mute <@!${user.id}>?`
240 )
pineafan73a7c4a2022-07-24 10:38:04 +0100241 .setColor("Danger")
242 .addCustomBoolean(
Skyler Grey75ea9172022-08-06 10:22:23 +0100243 "appeal",
244 "Create appeal ticket",
245 !(await areTicketsEnabled(interaction.guild.id)),
246 async () =>
247 await create(
248 interaction.guild,
249 interaction.options.getUser("user"),
250 interaction.user,
251 reason
252 ),
253 "An appeal ticket will be created when Confirm is clicked",
254 "CONTROL.TICKET",
255 createAppealTicket
256 )
257 .addCustomBoolean(
258 "notify",
259 "Notify user",
260 false,
261 null,
262 null,
263 "ICONS.NOTIFY." + (notify ? "ON" : "OFF"),
264 notify
265 )
pineafan73a7c4a2022-07-24 10:38:04 +0100266 .addReasonButton(reason ?? "")
pineafan63fc5e22022-08-04 22:04:10 +0100267 .send(true);
268 reason = reason ?? "";
269 if (confirmation.cancelled) return;
270 if (confirmation.success) break;
271 if (confirmation.newReason) reason = confirmation.newReason;
pineafan02ba0232022-07-24 22:16:15 +0100272 if (confirmation.components) {
pineafan63fc5e22022-08-04 22:04:10 +0100273 notify = confirmation.components.notify.active;
274 createAppealTicket = confirmation.components.appeal.active;
pineafan02ba0232022-07-24 22:16:15 +0100275 }
pineafan73a7c4a2022-07-24 10:38:04 +0100276 }
pineafan377794f2022-04-18 19:01:01 +0100277 if (confirmation.success) {
pineafan63fc5e22022-08-04 22:04:10 +0100278 let dmd = false;
pineafan5d1908e2022-02-28 21:34:47 +0000279 let dm;
pineafan63fc5e22022-08-04 22:04:10 +0100280 const config = await client.database.guilds.read(interaction.guild.id);
pineafan8b4b17f2022-02-27 20:42:52 +0000281 try {
pineafan02ba0232022-07-24 22:16:15 +0100282 if (notify) {
pineafan73a7c4a2022-07-24 10:38:04 +0100283 dm = await user.send({
Skyler Grey75ea9172022-08-06 10:22:23 +0100284 embeds: [
285 new EmojiEmbed()
286 .setEmoji("PUNISH.MUTE.RED")
287 .setTitle("Muted")
288 .setDescription(
289 `You have been muted in ${interaction.guild.name}` +
290 (reason
291 ? ` for:\n> ${reason}`
292 : ".\n\n" +
293 `You will be unmuted at: <t:${
294 Math.round(
295 new Date().getTime() / 1000
296 ) + muteTime
297 }:D> at <t:${
298 Math.round(
299 new Date().getTime() / 1000
300 ) + muteTime
301 }:T> (<t:${
302 Math.round(
303 new Date().getTime() / 1000
304 ) + muteTime
305 }:R>)`) +
306 (confirmation.components.appeal.response
307 ? `You can appeal this here: <#${confirmation.components.appeal.response}>`
308 : "")
309 )
310 .setStatus("Danger")
pineafan377794f2022-04-18 19:01:01 +0100311 ],
Skyler Grey75ea9172022-08-06 10:22:23 +0100312 components: [
313 new MessageActionRow().addComponents(
314 config.moderation.mute.text
315 ? [
316 new MessageButton()
317 .setStyle("LINK")
318 .setLabel(config.moderation.mute.text)
319 .setURL(config.moderation.mute.link)
320 ]
321 : []
322 )
323 ]
pineafan63fc5e22022-08-04 22:04:10 +0100324 });
325 dmd = true;
pineafan8b4b17f2022-02-27 20:42:52 +0000326 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100327 } catch {
328 dmd = false;
329 }
pineafan63fc5e22022-08-04 22:04:10 +0100330 const member = user;
331 let errors = 0;
pineafan8b4b17f2022-02-27 20:42:52 +0000332 try {
pineafane625d782022-05-09 18:04:32 +0100333 if (config.moderation.mute.timeout) {
Skyler Grey75ea9172022-08-06 10:22:23 +0100334 await member.timeout(
335 muteTime * 1000,
336 reason || "No reason provided"
337 );
pineafan73a7c4a2022-07-24 10:38:04 +0100338 if (config.moderation.mute.role !== null) {
pineafan63fc5e22022-08-04 22:04:10 +0100339 await member.roles.add(config.moderation.mute.role);
Skyler Grey75ea9172022-08-06 10:22:23 +0100340 await client.database.eventScheduler.schedule(
341 "naturalUnmute",
342 new Date().getTime() + muteTime * 1000,
343 {
344 guild: interaction.guild.id,
345 user: user.id,
346 expires: new Date().getTime() + muteTime * 1000
347 }
348 );
pineafan73a7c4a2022-07-24 10:38:04 +0100349 }
pineafane625d782022-05-09 18:04:32 +0100350 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100351 } catch {
352 errors++;
353 }
pineafan73a7c4a2022-07-24 10:38:04 +0100354 try {
355 if (config.moderation.mute.role !== null) {
pineafan63fc5e22022-08-04 22:04:10 +0100356 await member.roles.add(config.moderation.mute.role);
Skyler Grey75ea9172022-08-06 10:22:23 +0100357 await client.database.eventScheduler.schedule(
358 "unmuteRole",
359 new Date().getTime() + muteTime * 1000,
360 {
361 guild: interaction.guild.id,
362 user: user.id,
363 role: config.moderation.mute.role
364 }
365 );
pineafan73a7c4a2022-07-24 10:38:04 +0100366 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100367 } catch (e) {
368 console.log(e);
369 errors++;
370 }
pineafane23c4ec2022-07-27 21:56:27 +0100371 if (errors === 2) {
Skyler Grey75ea9172022-08-06 10:22:23 +0100372 await interaction.editReply({
373 embeds: [
374 new EmojiEmbed()
375 .setEmoji("PUNISH.MUTE.RED")
376 .setTitle("Mute")
377 .setDescription(
378 "Something went wrong and the user was not muted"
379 )
380 .setStatus("Danger")
381 ],
382 components: []
383 }); // TODO: make this clearer
pineafan63fc5e22022-08-04 22:04:10 +0100384 if (dmd) await dm.delete();
385 return;
pineafan8b4b17f2022-02-27 20:42:52 +0000386 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100387 await client.database.history.create(
388 "mute",
389 interaction.guild.id,
390 member.user,
391 interaction.user,
392 reason
393 );
394 const failed = !dmd && notify;
395 await interaction.editReply({
396 embeds: [
397 new EmojiEmbed()
398 .setEmoji(`PUNISH.MUTE.${failed ? "YELLOW" : "GREEN"}`)
399 .setTitle("Mute")
400 .setDescription(
401 "The member was muted" +
402 (failed ? ", but could not be notified" : "") +
403 (confirmation.components.appeal.response
404 ? ` and an appeal ticket was opened in <#${confirmation.components.appeal.response}>`
405 : "")
406 )
407 .setStatus(failed ? "Warning" : "Success")
408 ],
409 components: []
410 });
pineafan63fc5e22022-08-04 22:04:10 +0100411 const data = {
Skyler Grey75ea9172022-08-06 10:22:23 +0100412 meta: {
pineafan63fc5e22022-08-04 22:04:10 +0100413 type: "memberMute",
414 displayName: "Member Muted",
415 calculateType: "guildMemberPunish",
pineafan377794f2022-04-18 19:01:01 +0100416 color: NucleusColors.yellow,
pineafan63fc5e22022-08-04 22:04:10 +0100417 emoji: "PUNISH.WARN.YELLOW",
pineafan377794f2022-04-18 19:01:01 +0100418 timestamp: new Date().getTime()
419 },
420 list: {
pineafan73a7c4a2022-07-24 10:38:04 +0100421 memberId: entry(member.user.id, `\`${member.user.id}\``),
422 name: entry(member.user.id, renderUser(member.user)),
Skyler Grey75ea9172022-08-06 10:22:23 +0100423 mutedUntil: entry(
424 new Date().getTime() + muteTime * 1000,
425 renderDelta(new Date().getTime() + muteTime * 1000)
426 ),
427 muted: entry(
428 new Date().getTime(),
429 renderDelta(new Date().getTime() - 1000)
430 ),
431 mutedBy: entry(
432 interaction.member.user.id,
433 renderUser(interaction.member.user)
434 ),
pineafan63fc5e22022-08-04 22:04:10 +0100435 reason: entry(reason, reason ? reason : "*No reason provided*")
pineafan377794f2022-04-18 19:01:01 +0100436 },
437 hidden: {
438 guild: interaction.guild.id
439 }
pineafan63fc5e22022-08-04 22:04:10 +0100440 };
pineafan4edb7762022-06-26 19:21:04 +0100441 log(data);
pineafan8b4b17f2022-02-27 20:42:52 +0000442 } else {
Skyler Grey75ea9172022-08-06 10:22:23 +0100443 await interaction.editReply({
444 embeds: [
445 new EmojiEmbed()
446 .setEmoji("PUNISH.MUTE.GREEN")
447 .setTitle("Mute")
448 .setDescription("No changes were made")
449 .setStatus("Success")
450 ],
451 components: []
452 });
pineafan8b4b17f2022-02-27 20:42:52 +0000453 }
pineafan63fc5e22022-08-04 22:04:10 +0100454};
pineafan8b4b17f2022-02-27 20:42:52 +0000455
pineafanbd02b4a2022-08-05 22:01:38 +0100456const check = (interaction: CommandInteraction) => {
Skyler Grey75ea9172022-08-06 10:22:23 +0100457 const member = interaction.member as GuildMember;
458 const me = interaction.guild.me!;
459 const apply = interaction.options.getMember("user") as GuildMember;
460 if (member === null || me === null || apply === null)
461 throw "That member is not in the server";
pineafan63fc5e22022-08-04 22:04:10 +0100462 const memberPos = member.roles ? member.roles.highest.position : 0;
463 const mePos = me.roles ? me.roles.highest.position : 0;
464 const applyPos = apply.roles ? apply.roles.highest.position : 0;
pineafanc1c18792022-08-03 21:41:36 +0100465 // Do not allow muting the owner
Skyler Grey75ea9172022-08-06 10:22:23 +0100466 if (member.id === interaction.guild.ownerId)
467 throw "You cannot mute the owner of the server";
pineafan8b4b17f2022-02-27 20:42:52 +0000468 // Check if Nucleus can mute the member
Skyler Grey75ea9172022-08-06 10:22:23 +0100469 if (!(mePos > applyPos))
470 throw "I do not have a role higher than that member";
pineafan8b4b17f2022-02-27 20:42:52 +0000471 // Check if Nucleus has permission to mute
Skyler Grey75ea9172022-08-06 10:22:23 +0100472 if (!me.permissions.has("MODERATE_MEMBERS"))
473 throw "I do not have the *Moderate Members* permission";
pineafan8b4b17f2022-02-27 20:42:52 +0000474 // Do not allow muting Nucleus
pineafan63fc5e22022-08-04 22:04:10 +0100475 if (member.id === me.id) throw "I cannot mute myself";
pineafan8b4b17f2022-02-27 20:42:52 +0000476 // Allow the owner to mute anyone
pineafan63fc5e22022-08-04 22:04:10 +0100477 if (member.id === interaction.guild.ownerId) return true;
pineafan8b4b17f2022-02-27 20:42:52 +0000478 // Check if the user has moderate_members permission
Skyler Grey75ea9172022-08-06 10:22:23 +0100479 if (!member.permissions.has("MODERATE_MEMBERS"))
480 throw "You do not have the *Moderate Members* permission";
pineafan8b4b17f2022-02-27 20:42:52 +0000481 // Check if the user is below on the role list
Skyler Grey75ea9172022-08-06 10:22:23 +0100482 if (!(memberPos > applyPos))
483 throw "You do not have a role higher than that member";
pineafan8b4b17f2022-02-27 20:42:52 +0000484 // Allow mute
pineafan63fc5e22022-08-04 22:04:10 +0100485 return true;
486};
pineafan8b4b17f2022-02-27 20:42:52 +0000487
Skyler Grey75ea9172022-08-06 10:22:23 +0100488export { command, callback, check };