blob: 327ecdfdba2a02d4fd1eac64b492d3c9280cc847 [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;
Skyler Greyad002172022-08-16 18:48:26 +0100165 let timedOut = false;
166 let success = false;
167 while (!timedOut && !success) {
pineafan73a7c4a2022-07-24 10:38:04 +0100168 confirmation = await new confirmationMessage(interaction)
169 .setEmoji("PUNISH.MUTE.RED")
170 .setTitle("Mute")
Skyler Grey75ea9172022-08-06 10:22:23 +0100171 .setDescription(
172 keyValueList({
173 user: renderUser(user.user),
174 time: `${humanizeDuration(muteTime * 1000, {
175 round: true
176 })}`,
Skyler Grey11236ba2022-08-08 21:13:33 +0100177 reason: reason ? "\n> " + (reason ?? "").replaceAll("\n", "\n> ") : "*No reason provided*"
Skyler Grey75ea9172022-08-06 10:22:23 +0100178 }) +
179 "The user will be " +
180 serverSettingsDescription +
181 "\n" +
182 `The user **will${notify ? "" : " not"}** be notified\n\n` +
183 `Are you sure you want to mute <@!${user.id}>?`
184 )
pineafan73a7c4a2022-07-24 10:38:04 +0100185 .setColor("Danger")
186 .addCustomBoolean(
Skyler Grey75ea9172022-08-06 10:22:23 +0100187 "appeal",
188 "Create appeal ticket",
189 !(await areTicketsEnabled(interaction.guild.id)),
190 async () =>
Skyler Grey11236ba2022-08-08 21:13:33 +0100191 await create(interaction.guild, interaction.options.getUser("user"), interaction.user, reason),
Skyler Grey75ea9172022-08-06 10:22:23 +0100192 "An appeal ticket will be created when Confirm is clicked",
193 "CONTROL.TICKET",
194 createAppealTicket
195 )
196 .addCustomBoolean(
197 "notify",
198 "Notify user",
199 false,
200 null,
201 null,
202 "ICONS.NOTIFY." + (notify ? "ON" : "OFF"),
203 notify
204 )
pineafan73a7c4a2022-07-24 10:38:04 +0100205 .addReasonButton(reason ?? "")
pineafan63fc5e22022-08-04 22:04:10 +0100206 .send(true);
207 reason = reason ?? "";
Skyler Greyad002172022-08-16 18:48:26 +0100208 if (confirmation.cancelled) timedOut = true;
209 if (confirmation.success) success = true;
pineafan63fc5e22022-08-04 22:04:10 +0100210 if (confirmation.newReason) reason = confirmation.newReason;
pineafan02ba0232022-07-24 22:16:15 +0100211 if (confirmation.components) {
pineafan63fc5e22022-08-04 22:04:10 +0100212 notify = confirmation.components.notify.active;
213 createAppealTicket = confirmation.components.appeal.active;
pineafan02ba0232022-07-24 22:16:15 +0100214 }
pineafan73a7c4a2022-07-24 10:38:04 +0100215 }
Skyler Greyad002172022-08-16 18:48:26 +0100216 if (timedOut) return;
217 let dmd = false;
218 let dm;
219 const config = await client.database.guilds.read(interaction.guild.id);
220 try {
221 if (notify) {
222 dm = await user.send({
Skyler Grey75ea9172022-08-06 10:22:23 +0100223 embeds: [
224 new EmojiEmbed()
225 .setEmoji("PUNISH.MUTE.RED")
Skyler Greyad002172022-08-16 18:48:26 +0100226 .setTitle("Muted")
227 .setDescription(
228 `You have been muted in ${interaction.guild.name}` +
229 (reason
230 ? ` for:\n> ${reason}`
231 : ".\n\n" +
232 `You will be unmuted at: <t:${
233 Math.round(new Date().getTime() / 1000) + muteTime
234 }:D> at <t:${Math.round(new Date().getTime() / 1000) + muteTime}:T> (<t:${
235 Math.round(new Date().getTime() / 1000) + muteTime
236 }:R>)`) +
237 (confirmation.components.appeal.response
238 ? `You can appeal this here: <#${confirmation.components.appeal.response}>`
239 : "")
240 )
Skyler Grey75ea9172022-08-06 10:22:23 +0100241 .setStatus("Danger")
242 ],
Skyler Greyad002172022-08-16 18:48:26 +0100243 components: [
244 new MessageActionRow().addComponents(
245 config.moderation.mute.text
246 ? [
247 new MessageButton()
248 .setStyle("LINK")
249 .setLabel(config.moderation.mute.text)
250 .setURL(config.moderation.mute.link)
251 ]
252 : []
Skyler Grey75ea9172022-08-06 10:22:23 +0100253 )
Skyler Greyad002172022-08-16 18:48:26 +0100254 ]
255 });
256 dmd = true;
257 }
258 } catch {
259 dmd = false;
260 }
261 const member = user;
262 let errors = 0;
263 try {
264 if (config.moderation.mute.timeout) {
265 await member.timeout(muteTime * 1000, reason || "No reason provided");
266 if (config.moderation.mute.role !== null) {
267 await member.roles.add(config.moderation.mute.role);
268 await client.database.eventScheduler.schedule("naturalUnmute", new Date().getTime() + muteTime * 1000, {
269 guild: interaction.guild.id,
270 user: user.id,
271 expires: new Date().getTime() + muteTime * 1000
272 });
pineafan377794f2022-04-18 19:01:01 +0100273 }
Skyler Greyad002172022-08-16 18:48:26 +0100274 }
275 } catch {
276 errors++;
277 }
278 try {
279 if (config.moderation.mute.role !== null) {
280 await member.roles.add(config.moderation.mute.role);
281 await client.database.eventScheduler.schedule("unmuteRole", new Date().getTime() + muteTime * 1000, {
282 guild: interaction.guild.id,
283 user: user.id,
284 role: config.moderation.mute.role
285 });
286 }
287 } catch (e) {
288 console.log(e);
289 errors++;
290 }
291 if (errors === 2) {
Skyler Grey75ea9172022-08-06 10:22:23 +0100292 await interaction.editReply({
293 embeds: [
294 new EmojiEmbed()
Skyler Greyad002172022-08-16 18:48:26 +0100295 .setEmoji("PUNISH.MUTE.RED")
Skyler Grey75ea9172022-08-06 10:22:23 +0100296 .setTitle("Mute")
Skyler Greyad002172022-08-16 18:48:26 +0100297 .setDescription("Something went wrong and the user was not muted")
298 .setStatus("Danger")
Skyler Grey75ea9172022-08-06 10:22:23 +0100299 ],
300 components: []
Skyler Greyad002172022-08-16 18:48:26 +0100301 }); // TODO: make this clearer
302 if (dmd) await dm.delete();
303 return;
pineafan8b4b17f2022-02-27 20:42:52 +0000304 }
Skyler Greyad002172022-08-16 18:48:26 +0100305 await client.database.history.create("mute", interaction.guild.id, member.user, interaction.user, reason);
306 const failed = !dmd && notify;
307 await interaction.editReply({
308 embeds: [
309 new EmojiEmbed()
310 .setEmoji(`PUNISH.MUTE.${failed ? "YELLOW" : "GREEN"}`)
311 .setTitle("Mute")
312 .setDescription(
313 "The member was muted" +
314 (failed ? ", but could not be notified" : "") +
315 (confirmation.components.appeal.response
316 ? ` and an appeal ticket was opened in <#${confirmation.components.appeal.response}>`
317 : "")
318 )
319 .setStatus(failed ? "Warning" : "Success")
320 ],
321 components: []
322 });
323 const data = {
324 meta: {
325 type: "memberMute",
326 displayName: "Member Muted",
327 calculateType: "guildMemberPunish",
328 color: NucleusColors.yellow,
329 emoji: "PUNISH.WARN.YELLOW",
330 timestamp: new Date().getTime()
331 },
332 list: {
333 memberId: entry(member.user.id, `\`${member.user.id}\``),
334 name: entry(member.user.id, renderUser(member.user)),
335 mutedUntil: entry(
336 new Date().getTime() + muteTime * 1000,
337 renderDelta(new Date().getTime() + muteTime * 1000)
338 ),
339 muted: entry(new Date().getTime(), renderDelta(new Date().getTime() - 1000)),
340 mutedBy: entry(interaction.member.user.id, renderUser(interaction.member.user)),
341 reason: entry(reason, reason ? reason : "*No reason provided*")
342 },
343 hidden: {
344 guild: interaction.guild.id
345 }
346 };
347 log(data);
pineafan63fc5e22022-08-04 22:04:10 +0100348};
pineafan8b4b17f2022-02-27 20:42:52 +0000349
pineafanbd02b4a2022-08-05 22:01:38 +0100350const check = (interaction: CommandInteraction) => {
Skyler Grey75ea9172022-08-06 10:22:23 +0100351 const member = interaction.member as GuildMember;
352 const me = interaction.guild.me!;
353 const apply = interaction.options.getMember("user") as GuildMember;
pineafan3a02ea32022-08-11 21:35:04 +0100354 if (member === null || me === null || apply === null) throw new Error("That member is not in the server");
pineafan63fc5e22022-08-04 22:04:10 +0100355 const memberPos = member.roles ? member.roles.highest.position : 0;
356 const mePos = me.roles ? me.roles.highest.position : 0;
357 const applyPos = apply.roles ? apply.roles.highest.position : 0;
pineafanc1c18792022-08-03 21:41:36 +0100358 // Do not allow muting the owner
pineafan3a02ea32022-08-11 21:35:04 +0100359 if (member.id === interaction.guild.ownerId) throw new Error("You cannot mute the owner of the server");
pineafan8b4b17f2022-02-27 20:42:52 +0000360 // Check if Nucleus can mute the member
pineafan3a02ea32022-08-11 21:35:04 +0100361 if (!(mePos > applyPos)) throw new Error("I do not have a role higher than that member");
pineafan8b4b17f2022-02-27 20:42:52 +0000362 // Check if Nucleus has permission to mute
pineafan3a02ea32022-08-11 21:35:04 +0100363 if (!me.permissions.has("MODERATE_MEMBERS")) throw new Error("I do not have the *Moderate Members* permission");
pineafan8b4b17f2022-02-27 20:42:52 +0000364 // Do not allow muting Nucleus
pineafan3a02ea32022-08-11 21:35:04 +0100365 if (member.id === me.id) throw new Error("I cannot mute myself");
pineafan8b4b17f2022-02-27 20:42:52 +0000366 // Allow the owner to mute anyone
pineafan63fc5e22022-08-04 22:04:10 +0100367 if (member.id === interaction.guild.ownerId) return true;
pineafan8b4b17f2022-02-27 20:42:52 +0000368 // Check if the user has moderate_members permission
pineafan3a02ea32022-08-11 21:35:04 +0100369 if (!member.permissions.has("MODERATE_MEMBERS"))
370 throw new Error("You do not have the *Moderate Members* permission");
pineafan8b4b17f2022-02-27 20:42:52 +0000371 // Check if the user is below on the role list
pineafan3a02ea32022-08-11 21:35:04 +0100372 if (!(memberPos > applyPos)) throw new Error("You do not have a role higher than that member");
pineafan8b4b17f2022-02-27 20:42:52 +0000373 // Allow mute
pineafan63fc5e22022-08-04 22:04:10 +0100374 return true;
375};
pineafan8b4b17f2022-02-27 20:42:52 +0000376
Skyler Grey75ea9172022-08-06 10:22:23 +0100377export { command, callback, check };