blob: d3d24c9bc0f59cf45180d58b9afa252b2b6a78ea [file] [log] [blame]
TheCodedProf8b3da212023-02-02 15:09:55 -05001import type Discord from "discord.js";
TheCodedProf5b53a8c2023-02-03 15:40:26 -05002import { ActionRowBuilder,
3 AnySelectMenuInteraction,
4 APIMessageComponentEmoji,
5 ButtonBuilder,
6 ButtonInteraction,
7 ButtonStyle,
8 ChannelSelectMenuBuilder,
9 ChannelSelectMenuInteraction,
10 CommandInteraction,
11 Interaction,
12 Message,
13 MessageComponentInteraction,
14 ModalBuilder,
15 ModalSubmitInteraction,
16 RoleSelectMenuBuilder,
17 RoleSelectMenuInteraction,
18 StringSelectMenuBuilder,
19 StringSelectMenuInteraction,
20 StringSelectMenuOptionBuilder,
21 TextInputBuilder,
22 TextInputStyle,
23 UserSelectMenuBuilder,
24 UserSelectMenuInteraction
25} from "discord.js";
TheCodedProf8b3da212023-02-02 15:09:55 -050026import type { SlashCommandSubcommandBuilder } from "discord.js";
27import { LoadingEmbed } from "../../utils/defaults.js";
28import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
29import client from "../../utils/client.js";
30import getEmojiByName from "../../utils/getEmojiByName.js";
TheCodedProf5b53a8c2023-02-03 15:40:26 -050031import { modalInteractionCollector } from "../../utils/dualCollector.js";
TheCodedProf8b3da212023-02-02 15:09:55 -050032
TheCodedProf486bca32023-02-02 16:49:44 -050033
TheCodedProf8b3da212023-02-02 15:09:55 -050034const command = (builder: SlashCommandSubcommandBuilder) =>
35 builder.setName("automod").setDescription("Setting for automatic moderation features");
36
37
38const emojiFromBoolean = (bool: boolean, id?: string) => bool ? getEmojiByName("CONTROL.TICK", id) : getEmojiByName("CONTROL.CROSS", id);
39
TheCodedProf486bca32023-02-02 16:49:44 -050040const listToAndMore = (list: string[], max: number) => {
41 // PineappleFan, Coded, Mini (and 10 more)
42 if(list.length > max) {
43 return list.slice(0, max).join(", ") + ` (and ${list.length - max} more)`;
44 }
45 return list.join(", ");
46}
47
48const toSelectMenu = async (interaction: StringSelectMenuInteraction, m: Message, ids: string[], type: "member" | "role" | "channel", title: string): Promise<string[]> => {
49
50 const back = new ActionRowBuilder<ButtonBuilder>().addComponents(new ButtonBuilder().setCustomId("back").setLabel("Back").setStyle(ButtonStyle.Secondary).setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji));
TheCodedProf486bca32023-02-02 16:49:44 -050051 let closed;
52 do {
53 let render: string[] = []
54 let mapped: string[] = [];
55 let menu: UserSelectMenuBuilder | RoleSelectMenuBuilder | ChannelSelectMenuBuilder;
56 switch(type) {
PineaFanb0d0c242023-02-05 10:59:45 +000057 case "member": {
TheCodedProf486bca32023-02-02 16:49:44 -050058 menu = new UserSelectMenuBuilder().setCustomId("user").setPlaceholder("Select users").setMaxValues(25);
59 mapped = await Promise.all(ids.map(async (id) => { return (await client.users.fetch(id).then(user => user.tag)) || "Unknown User" }));
60 render = ids.map(id => client.logger.renderUser(id))
61 break;
PineaFanb0d0c242023-02-05 10:59:45 +000062 }
63 case "role": {
TheCodedProf486bca32023-02-02 16:49:44 -050064 menu = new RoleSelectMenuBuilder().setCustomId("role").setPlaceholder("Select roles").setMaxValues(25);
PineaFanb0d0c242023-02-05 10:59:45 +000065 mapped = await Promise.all(ids.map(async (id) => { return (await interaction.guild!.roles.fetch(id).then(role => role?.name ?? "Unknown Role"))}));
TheCodedProf486bca32023-02-02 16:49:44 -050066 render = ids.map(id => client.logger.renderRole(id, interaction.guild!))
67 break;
PineaFanb0d0c242023-02-05 10:59:45 +000068 }
69 case "channel": {
TheCodedProf486bca32023-02-02 16:49:44 -050070 menu = new ChannelSelectMenuBuilder().setCustomId("channel").setPlaceholder("Select channels").setMaxValues(25);
PineaFanb0d0c242023-02-05 10:59:45 +000071 mapped = await Promise.all(ids.map(async (id) => { return (await interaction.guild!.channels.fetch(id).then(channel => channel?.name ?? "Unknown Role")) }));
TheCodedProf486bca32023-02-02 16:49:44 -050072 render = ids.map(id => client.logger.renderChannel(id))
73 break;
PineaFanb0d0c242023-02-05 10:59:45 +000074 }
TheCodedProf486bca32023-02-02 16:49:44 -050075 }
76 const removeOptions = new ActionRowBuilder<StringSelectMenuBuilder>()
77 .addComponents(
78 new StringSelectMenuBuilder()
79 .setCustomId("remove")
80 .setPlaceholder("Remove")
81 .addOptions(mapped.map((name, i) => new StringSelectMenuOptionBuilder().setLabel(name).setValue(ids[i]!)))
82 .setDisabled(ids.length === 0)
83 );
84
85 const embed = new EmojiEmbed()
86 .setTitle(title)
87 .setEmoji(getEmojiByName("GUILD.SETTINGS.GREEN"))
88 .setDescription(`Select ${type}s:\n\nCurrent:\n` + (render.length > 0 ? render.join("\n") : "None"))
89 .setStatus("Success");
PineaFanb0d0c242023-02-05 10:59:45 +000090 const components: ActionRowBuilder<
TheCodedProf486bca32023-02-02 16:49:44 -050091 StringSelectMenuBuilder |
92 ButtonBuilder |
93 ChannelSelectMenuBuilder |
94 UserSelectMenuBuilder |
95 RoleSelectMenuBuilder
96 >[] = [new ActionRowBuilder<typeof menu>().addComponents(menu)]
97 if(ids.length > 0) components.push(removeOptions);
98 components.push(back);
99
100 await interaction.editReply({embeds: [embed], components: components})
101
102 let i: AnySelectMenuInteraction | ButtonInteraction;
103 try {
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500104 i = await m.awaitMessageComponent({filter: i => i.user.id === interaction.user.id, time: 300000});
TheCodedProf486bca32023-02-02 16:49:44 -0500105 } catch(e) {
106 closed = true;
PineaFanb0d0c242023-02-05 10:59:45 +0000107 continue;
TheCodedProf486bca32023-02-02 16:49:44 -0500108 }
109
110 if(i.isButton()) {
111 await i.deferUpdate();
112 if(i.customId === "back") {
113 closed = true;
114 break;
115 }
116 } else if(i.isStringSelectMenu()) {
117 await i.deferUpdate();
118 if(i.customId === "remove") {
119 ids = ids.filter(id => id !== (i as StringSelectMenuInteraction).values[0]);
120 if(ids.length === 0) {
121 menu.data.disabled = true;
122 }
123 }
124 } else {
125 await i.deferUpdate();
126 if(i.customId === "user") {
127 ids = ids.concat((i as UserSelectMenuInteraction).values);
128 } else if(i.customId === "role") {
129 ids = ids.concat((i as RoleSelectMenuInteraction).values);
130 } else if(i.customId === "channel") {
131 ids = ids.concat((i as ChannelSelectMenuInteraction).values);
132 }
133 }
134
135 } while(!closed)
136 return ids;
137}
TheCodedProf8b3da212023-02-02 15:09:55 -0500138
139const imageMenu = async (interaction: StringSelectMenuInteraction, m: Message, current: {
140 NSFW: boolean,
141 size: boolean
142}): Promise<{NSFW: boolean, size: boolean}> => {
143 let closed = false;
144 do {
145 const options = new ActionRowBuilder<ButtonBuilder>()
146 .addComponents(
147 new ButtonBuilder()
148 .setCustomId("back")
149 .setLabel("Back")
150 .setStyle(ButtonStyle.Secondary)
151 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
152 new ButtonBuilder()
153 .setCustomId("nsfw")
154 .setLabel("NSFW")
155 .setStyle(current.NSFW ? ButtonStyle.Success : ButtonStyle.Danger)
156 .setEmoji(emojiFromBoolean(current.NSFW, "id") as APIMessageComponentEmoji),
157 new ButtonBuilder()
158 .setCustomId("size")
159 .setLabel("Size")
160 .setStyle(current.size ? ButtonStyle.Success : ButtonStyle.Danger)
161 .setEmoji(emojiFromBoolean(current.size, "id") as APIMessageComponentEmoji)
162 )
163
164 const embed = new EmojiEmbed()
165 .setTitle("Image Settings")
166 .setDescription(
167 `${emojiFromBoolean(current.NSFW)} **NSFW**\n` +
168 `${emojiFromBoolean(current.size)} **Size**\n`
169 )
170
171 await interaction.editReply({embeds: [embed], components: [options]});
172
173 let i: ButtonInteraction;
174 try {
175 i = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction;
176 } catch (e) {
177 return current;
178 }
179 await i.deferUpdate();
180 switch(i.customId) {
PineaFanb0d0c242023-02-05 10:59:45 +0000181 case "back": {
TheCodedProf8b3da212023-02-02 15:09:55 -0500182 closed = true;
183 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000184 }
185 case "nsfw": {
TheCodedProf8b3da212023-02-02 15:09:55 -0500186 current.NSFW = !current.NSFW;
187 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000188 }
189 case "size": {
TheCodedProf8b3da212023-02-02 15:09:55 -0500190 current.size = !current.size;
191 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000192 }
TheCodedProf8b3da212023-02-02 15:09:55 -0500193 }
194 } while(!closed);
195 return current;
196}
197
198const wordMenu = async (interaction: StringSelectMenuInteraction, m: Message, current: {
199 enabled: boolean,
200 words: {strict: string[], loose: string[]},
TheCodedProf486bca32023-02-02 16:49:44 -0500201 allowed: {users: string[], roles: string[], channels: string[]}
TheCodedProf8b3da212023-02-02 15:09:55 -0500202}): Promise<{
203 enabled: boolean,
204 words: {strict: string[], loose: string[]},
TheCodedProf486bca32023-02-02 16:49:44 -0500205 allowed: {users: string[], roles: string[], channels: string[]}
TheCodedProf8b3da212023-02-02 15:09:55 -0500206}> => {
TheCodedProf486bca32023-02-02 16:49:44 -0500207 let closed = false;
208 do {
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500209 const buttons = new ActionRowBuilder<ButtonBuilder>()
210 .addComponents(
211 new ButtonBuilder()
212 .setCustomId("back")
213 .setLabel("Back")
214 .setStyle(ButtonStyle.Secondary)
215 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
216 new ButtonBuilder()
217 .setCustomId("enabled")
218 .setLabel("Enabled")
219 .setStyle(current.enabled ? ButtonStyle.Success : ButtonStyle.Danger)
220 .setEmoji(emojiFromBoolean(current.enabled, "id") as APIMessageComponentEmoji),
221 );
222
223 const selectMenu = new ActionRowBuilder<StringSelectMenuBuilder>()
224 .addComponents(
225 new StringSelectMenuBuilder()
226 .setCustomId("edit")
227 .setPlaceholder("Edit... ")
228 .addOptions(
229 new StringSelectMenuOptionBuilder()
230 .setLabel("Words")
231 .setDescription("Edit your list of words to filter")
232 .setValue("words"),
233 new StringSelectMenuOptionBuilder()
234 .setLabel("Allowed Users")
235 .setDescription("Users who will be unaffected by the word filter")
236 .setValue("allowedUsers"),
237 new StringSelectMenuOptionBuilder()
238 .setLabel("Allowed Roles")
239 .setDescription("Roles that will be unaffected by the word filter")
240 .setValue("allowedRoles"),
241 new StringSelectMenuOptionBuilder()
242 .setLabel("Allowed Channels")
243 .setDescription("Channels where the word filter will not apply")
244 .setValue("allowedChannels")
245 )
246 .setDisabled(!current.enabled)
247 );
248
249 const embed = new EmojiEmbed()
250 .setTitle("Word Filters")
251 .setDescription(
252 `${emojiFromBoolean(current.enabled)} **Enabled**\n` +
253 `**Strict Words:** ${listToAndMore(current.words.strict, 5)}\n` +
254 `**Loose Words:** ${listToAndMore(current.words.loose, 5)}\n\n` +
255 `**Users:** ` + listToAndMore(current.allowed.users.map(user => `<@${user}>`), 5) + `\n` +
256 `**Roles:** ` + listToAndMore(current.allowed.roles.map(role => `<@&${role}>`), 5) + `\n` +
257 `**Channels:** ` + listToAndMore(current.allowed.channels.map(channel => `<#${channel}>`), 5)
258 )
259 .setStatus("Success")
260 .setEmoji("GUILD.SETTINGS.GREEN")
261
262 await interaction.editReply({embeds: [embed], components: [selectMenu, buttons]});
263
264 let i: ButtonInteraction | StringSelectMenuInteraction;
265 try {
266 i = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction | StringSelectMenuInteraction;
267 } catch (e) {
268 closed = true;
269 break;
270 }
271
272 if(i.isButton()) {
273 await i.deferUpdate();
274 switch(i.customId) {
PineaFanb0d0c242023-02-05 10:59:45 +0000275 case "back": {
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500276 closed = true;
277 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000278 }
279 case "enabled": {
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500280 current.enabled = !current.enabled;
281 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000282 }
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500283 }
284 } else {
285 switch(i.values[0]) {
PineaFanb0d0c242023-02-05 10:59:45 +0000286 case "words": {
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500287 await interaction.editReply({embeds: [new EmojiEmbed()
288 .setTitle("Word Filter")
289 .setDescription("Modal opened. If you can't see it, click back and try again.")
290 .setStatus("Success")
291 .setEmoji("GUILD.SETTINGS.GREEN")
292 ], components: [new ActionRowBuilder<ButtonBuilder>().addComponents(new ButtonBuilder()
293 .setLabel("Back")
294 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
295 .setStyle(ButtonStyle.Primary)
296 .setCustomId("back")
297 )]})
298 const modal = new ModalBuilder()
299 .setTitle("Word Filter")
300 .setCustomId("wordFilter")
301 .addComponents(
302 new ActionRowBuilder<TextInputBuilder>()
303 .addComponents(
304 new TextInputBuilder()
305 .setCustomId("wordStrict")
306 .setLabel("Strict Words")
307 .setPlaceholder("Matches anywhere in the message, including surrounded by other characters")
PineaFanb0d0c242023-02-05 10:59:45 +0000308 .setValue(current.words.strict.join(", "))
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500309 .setStyle(TextInputStyle.Paragraph)
310 .setRequired(false)
311 ),
312 new ActionRowBuilder<TextInputBuilder>()
313 .addComponents(
314 new TextInputBuilder()
315 .setCustomId("wordLoose")
316 .setLabel("Loose Words")
317 .setPlaceholder("Matches only if the word is by itself, surrounded by spaces or punctuation")
PineaFanb0d0c242023-02-05 10:59:45 +0000318 .setValue(current.words.loose.join(", "))
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500319 .setStyle(TextInputStyle.Paragraph)
320 .setRequired(false)
321 )
322 )
323
324 await i.showModal(modal);
325 let out;
326 try {
327 out = await modalInteractionCollector(
328 m,
329 (m: Interaction) =>
330 (m as MessageComponentInteraction | ModalSubmitInteraction).channelId === interaction.channelId,
331 (m) => m.customId === "back"
332 );
333 } catch (e) {
334 break;
335 }
336 if (!out) break;
337 if(out.isButton()) break;
338 current.words.strict = out.fields.getTextInputValue("wordStrict")
339 .split(",").map(s => s.trim()).filter(s => s.length > 0);
340 current.words.loose = out.fields.getTextInputValue("wordLoose")
341 .split(",").map(s => s.trim()).filter(s => s.length > 0);
342 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000343 }
344 case "allowedUsers": {
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500345 await i.deferUpdate();
346 current.allowed.users = await toSelectMenu(interaction, m, current.allowed.users, "member", "Word Filter");
347 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000348 }
349 case "allowedRoles": {
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500350 await i.deferUpdate();
351 current.allowed.roles = await toSelectMenu(interaction, m, current.allowed.roles, "role", "Word Filter");
352 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000353 }
354 case "allowedChannels": {
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500355 await i.deferUpdate();
356 current.allowed.channels = await toSelectMenu(interaction, m, current.allowed.channels, "channel", "Word Filter");
357 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000358 }
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500359 }
360 }
TheCodedProf486bca32023-02-02 16:49:44 -0500361 } while(!closed);
362 return current;
TheCodedProf8b3da212023-02-02 15:09:55 -0500363}
364
365const inviteMenu = async (interaction: StringSelectMenuInteraction, m: Message, current: {
366 enabled: boolean,
TheCodedProf486bca32023-02-02 16:49:44 -0500367 allowed: {users: string[], roles: string[], channels: string[]}
TheCodedProf8b3da212023-02-02 15:09:55 -0500368}): Promise<{
369 enabled: boolean,
TheCodedProf486bca32023-02-02 16:49:44 -0500370 allowed: {users: string[], roles: string[], channels: string[]}
TheCodedProf8b3da212023-02-02 15:09:55 -0500371}> => {
372
TheCodedProf486bca32023-02-02 16:49:44 -0500373 let closed = false;
374 do {
375 const buttons = new ActionRowBuilder<ButtonBuilder>()
376 .addComponents(
377 new ButtonBuilder()
378 .setCustomId("back")
379 .setLabel("Back")
380 .setStyle(ButtonStyle.Secondary)
381 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
382 new ButtonBuilder()
383 .setCustomId("enabled")
384 .setLabel(current.enabled ? "Enabled" : "Disabled")
385 .setStyle(current.enabled ? ButtonStyle.Success : ButtonStyle.Danger)
386 .setEmoji(emojiFromBoolean(current.enabled, "id") as APIMessageComponentEmoji)
387 );
388 const menu = new ActionRowBuilder<StringSelectMenuBuilder>()
389 .addComponents(
390 new StringSelectMenuBuilder()
391 .setCustomId("toEdit")
392 .setPlaceholder("Edit your allow list")
393 .addOptions(
394 new StringSelectMenuOptionBuilder()
395 .setLabel("Users")
396 .setDescription("Users that are allowed to send invites")
397 .setValue("users"),
398 new StringSelectMenuOptionBuilder()
399 .setLabel("Roles")
400 .setDescription("Roles that are allowed to send invites")
401 .setValue("roles"),
402 new StringSelectMenuOptionBuilder()
403 .setLabel("Channels")
404 .setDescription("Channels that anyone is allowed to send invites in")
405 .setValue("channels")
406 ).setDisabled(!current.enabled)
407 )
408
409 const embed = new EmojiEmbed()
410 .setTitle("Invite Settings")
411 .setDescription(
412 "Automatically deletes invites sent by users (outside of staff members and self promotion channels)" + `\n\n` +
413 `${emojiFromBoolean(current.enabled)} **${current.enabled ? "Enabled" : "Disabled"}**\n\n` +
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500414 `**Users:** ` + listToAndMore(current.allowed.users.map(user => `<@${user}>`), 5) + `\n` +
415 `**Roles:** ` + listToAndMore(current.allowed.roles.map(role => `<@&${role}>`), 5) + `\n` +
416 `**Channels:** ` + listToAndMore(current.allowed.channels.map(channel => `<#${channel}>`), 5)
TheCodedProf486bca32023-02-02 16:49:44 -0500417 )
418 .setStatus("Success")
419 .setEmoji("GUILD.SETTINGS.GREEN")
420
421
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500422 await interaction.editReply({embeds: [embed], components: [menu, buttons]});
TheCodedProf486bca32023-02-02 16:49:44 -0500423
424 let i: ButtonInteraction | StringSelectMenuInteraction;
425 try {
426 i = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction | StringSelectMenuInteraction;
427 } catch (e) {
428 return current;
429 }
430
431 if(i.isButton()) {
432 await i.deferUpdate();
433 switch(i.customId) {
PineaFanb0d0c242023-02-05 10:59:45 +0000434 case "back": {
TheCodedProf486bca32023-02-02 16:49:44 -0500435 closed = true;
436 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000437 }
438 case "enabled": {
TheCodedProf486bca32023-02-02 16:49:44 -0500439 current.enabled = !current.enabled;
440 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000441 }
TheCodedProf486bca32023-02-02 16:49:44 -0500442 }
443 } else {
444 await i.deferUpdate();
445 switch(i.values[0]) {
PineaFanb0d0c242023-02-05 10:59:45 +0000446 case "users": {
TheCodedProf486bca32023-02-02 16:49:44 -0500447 current.allowed.users = await toSelectMenu(interaction, m, current.allowed.users, "member", "Invite Settings");
448 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000449 }
450 case "roles": {
TheCodedProf486bca32023-02-02 16:49:44 -0500451 current.allowed.roles = await toSelectMenu(interaction, m, current.allowed.roles, "role", "Invite Settings");
452 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000453 }
454 case "channels": {
TheCodedProf486bca32023-02-02 16:49:44 -0500455 current.allowed.channels = await toSelectMenu(interaction, m, current.allowed.channels, "channel", "Invite Settings");
456 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000457 }
TheCodedProf486bca32023-02-02 16:49:44 -0500458 }
459 }
460
461 } while(!closed);
462 return current;
TheCodedProf8b3da212023-02-02 15:09:55 -0500463}
464
465const mentionMenu = async (interaction: StringSelectMenuInteraction, m: Message, current: {
466 mass: number,
467 everyone: boolean,
468 roles: boolean,
469 allowed: {
470 roles: string[],
471 rolesToMention: string[],
472 users: string[],
473 channels: string[]
474 }
475}): Promise<{
476 mass: number,
477 everyone: boolean,
478 roles: boolean,
479 allowed: {
480 roles: string[],
481 rolesToMention: string[],
482 users: string[],
483 channels: string[]
484 }
485}> => {
TheCodedProf486bca32023-02-02 16:49:44 -0500486 let closed = false;
487
488 do {
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500489
490 const buttons = new ActionRowBuilder<ButtonBuilder>()
491 .addComponents(
492 new ButtonBuilder()
493 .setCustomId("back")
494 .setLabel("Back")
495 .setStyle(ButtonStyle.Secondary)
496 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
497 new ButtonBuilder()
498 .setCustomId("everyone")
499 .setLabel(current.everyone ? "Everyone" : "No one")
500 .setStyle(current.everyone ? ButtonStyle.Success : ButtonStyle.Danger)
501 .setEmoji(emojiFromBoolean(current.everyone, "id") as APIMessageComponentEmoji),
502 new ButtonBuilder()
503 .setCustomId("roles")
504 .setLabel(current.roles ? "Roles" : "No roles")
505 .setStyle(current.roles ? ButtonStyle.Success : ButtonStyle.Danger)
506 .setEmoji(emojiFromBoolean(current.roles, "id") as APIMessageComponentEmoji)
507 );
508 const menu = new ActionRowBuilder<StringSelectMenuBuilder>()
509 .addComponents(
510 new StringSelectMenuBuilder()
511 .setCustomId("toEdit")
512 .setPlaceholder("Edit mention settings")
513 .addOptions(
514 new StringSelectMenuOptionBuilder()
515 .setLabel("Mass Mention Amount")
516 .setDescription("The amount of mentions before the bot will delete the message")
517 .setValue("mass"),
518 new StringSelectMenuOptionBuilder()
519 .setLabel("Roles")
520 .setDescription("Roles that are able to be mentioned")
521 .setValue("roles"),
522 )
523 )
524
525 const allowedMenu = new ActionRowBuilder<StringSelectMenuBuilder>()
526 .addComponents(
527 new StringSelectMenuBuilder()
528 .setCustomId("allowed")
529 .setPlaceholder("Edit exceptions")
530 .addOptions(
531 new StringSelectMenuOptionBuilder()
532 .setLabel("Users")
533 .setDescription("Users that are unaffected by the mention filter")
534 .setValue("users"),
535 new StringSelectMenuOptionBuilder()
536 .setLabel("Roles")
537 .setDescription("Roles that are unaffected by the mention filter")
538 .setValue("roles"),
539 new StringSelectMenuOptionBuilder()
540 .setLabel("Channels")
541 .setDescription("Channels where anyone is unaffected by the mention filter")
542 .setValue("channels")
543 )
544 )
545
546 const embed = new EmojiEmbed()
547 .setTitle("Mention Settings")
548 .setDescription(
549 `Log when members mention:\n` +
550 `${emojiFromBoolean(true)} **${current.mass}+ members** in one message\n` +
551 `${emojiFromBoolean(current.everyone)} **Everyone**\n` +
552 `${emojiFromBoolean(current.roles)} **Roles**\n` +
553 (current.allowed.rolesToMention.length > 0 ? `> *Except for ${listToAndMore(current.allowed.rolesToMention.map(r => `<@&${r}>`), 3)}*\n` : "") +
554 "\n" +
555 `Except if...\n` +
556 `> ${current.allowed.users.length > 0 ? `Member is: ${listToAndMore(current.allowed.users.map(u => `<@${u}>`), 3)}\n` : ""}` +
557 `> ${current.allowed.roles.length > 0 ? `Member has role: ${listToAndMore(current.allowed.roles.map(r => `<@&${r}>`), 3)}\n` : ""}` +
558 `> ${current.allowed.channels.length > 0 ? `In channel: ${listToAndMore(current.allowed.channels.map(c => `<#${c}>`), 3)}\n` : ""}`
559 )
560 .setStatus("Success")
561 .setEmoji("GUILD.SETTINGS.GREEN")
562
563 await interaction.editReply({embeds: [embed], components: [menu, allowedMenu, buttons]});
564
565 let i: ButtonInteraction | StringSelectMenuInteraction;
566 try {
567 i = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction | StringSelectMenuInteraction;
568 } catch (e) {
569 closed = true;
570 break;
571 }
572
573 if(i.isButton()) {
574 await i.deferUpdate();
575 switch (i.customId) {
PineaFanb0d0c242023-02-05 10:59:45 +0000576 case "back": {
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500577 closed = true;
578 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000579 }
580 case "everyone": {
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500581 current.everyone = !current.everyone;
582 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000583 }
584 case "roles": {
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500585 current.roles = !current.roles;
586 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000587 }
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500588 }
589 } else {
590 switch (i.customId) {
PineaFanb0d0c242023-02-05 10:59:45 +0000591 case "toEdit": {
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500592 switch (i.values[0]) {
PineaFanb0d0c242023-02-05 10:59:45 +0000593 case "mass": {
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500594 await interaction.editReply({embeds: [new EmojiEmbed()
595 .setTitle("Word Filter")
596 .setDescription("Modal opened. If you can't see it, click back and try again.")
597 .setStatus("Success")
598 .setEmoji("GUILD.SETTINGS.GREEN")
599 ], components: [new ActionRowBuilder<ButtonBuilder>().addComponents(new ButtonBuilder()
600 .setLabel("Back")
601 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
602 .setStyle(ButtonStyle.Primary)
603 .setCustomId("back")
604 )]})
605 const modal = new ModalBuilder()
606 .setTitle("Mass Mention Amount")
607 .setCustomId("mass")
608 .addComponents(
609 new ActionRowBuilder<TextInputBuilder>()
610 .addComponents(
611 new TextInputBuilder()
612 .setCustomId("mass")
613 .setPlaceholder("Amount")
614 .setMinLength(1)
615 .setMaxLength(3)
616 .setStyle(TextInputStyle.Short)
617 )
618 )
619 await i.showModal(modal);
620 let out;
621 try {
622 out = await modalInteractionCollector(
623 m,
624 (m: Interaction) =>
625 (m as MessageComponentInteraction | ModalSubmitInteraction).channelId === interaction.channelId,
626 (m) => m.customId === "back"
627 );
628 } catch (e) {
629 break;
630 }
631 if (!out) break;
632 if(out.isButton()) break;
633 current.mass = parseInt(out.fields.getTextInputValue("mass"));
634 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000635 }
636 case "roles": {
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500637 await i.deferUpdate();
638 current.allowed.rolesToMention = await toSelectMenu(interaction, m, current.allowed.rolesToMention, "role", "Mention Settings");
639 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000640 }
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500641 }
642 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000643 }
644 case "allowed": {
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500645 await i.deferUpdate();
646 switch (i.values[0]) {
PineaFanb0d0c242023-02-05 10:59:45 +0000647 case "users": {
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500648 current.allowed.users = await toSelectMenu(interaction, m, current.allowed.users, "member", "Mention Settings");
649 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000650 }
651 case "roles": {
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500652 current.allowed.roles = await toSelectMenu(interaction, m, current.allowed.roles, "role", "Mention Settings");
653 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000654 }
655 case "channels": {
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500656 current.allowed.channels = await toSelectMenu(interaction, m, current.allowed.channels, "channel", "Mention Settings");
657 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000658 }
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500659 }
660 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000661 }
TheCodedProf5b53a8c2023-02-03 15:40:26 -0500662 }
663 }
664
TheCodedProf486bca32023-02-02 16:49:44 -0500665 } while(!closed);
666 return current
TheCodedProf8b3da212023-02-02 15:09:55 -0500667}
668
TheCodedProfad0b8202023-02-14 14:27:09 -0500669const cleanMenu = async (interaction: StringSelectMenuInteraction, m: Message, current: {
670 channels: string[],
671 allowed: {
672 roles: string[],
673 user: string[]
674 }
675}): Promise<{
676 channels: string[],
677 allowed: {
678 roles: string[],
679 user: string[]
680 }
681}> => {
682 let closed = false;
683 if(!current) current = {channels: [], allowed: {roles: [], user: []}};
684 if(!current.channels) current.channels = [];
685 if(!current.allowed) current.allowed = {roles: [], user: []};
686
687 const channelMenu = new ActionRowBuilder<ChannelSelectMenuBuilder>()
688 .addComponents(
689 new ChannelSelectMenuBuilder()
690 .setCustomId("toAdd")
691 .setPlaceholder("Select a channel")
692 )
693
694 const allowedMenu = new ActionRowBuilder<StringSelectMenuBuilder>()
695 .addComponents(
696 new StringSelectMenuBuilder()
697 .setCustomId("allowed")
698 .setPlaceholder("Edit exceptions")
699 .addOptions(
700 new StringSelectMenuOptionBuilder()
701 .setLabel("Users")
702 .setDescription("Users that are unaffected by the mention filter")
703 .setValue("users"),
704 new StringSelectMenuOptionBuilder()
705 .setLabel("Roles")
706 .setDescription("Roles that are unaffected by the mention filter")
707 .setValue("roles")
708 )
709 )
710
711 do {
712
713 const buttons = new ActionRowBuilder<ButtonBuilder>()
714 .addComponents(
715 new ButtonBuilder()
716 .setCustomId("back")
717 .setLabel("Back")
718 .setStyle(ButtonStyle.Primary)
719 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
720 )
721
722 const embed = new EmojiEmbed()
723 .setTitle("Clean Settings")
724 .setEmoji("GUILD.SETTINGS.GREEN")
725 .setDescription(
726 `Current clean channels:\n\n` +
727 `${current.channels.length > 0 ? listToAndMore(current.channels.map(c => `<#${c}>`), 10) : "None"}\n\n`
728 )
729 .setStatus("Success")
730
731
732 await interaction.editReply({embeds: [embed], components: [channelMenu, allowedMenu, buttons]});
733
734 let i: ButtonInteraction | ChannelSelectMenuInteraction;
735 try {
736 i = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction | ChannelSelectMenuInteraction;
737 } catch (e) {
738 closed = true;
739 break;
740 }
741 await i.deferUpdate();
742 if(i.isButton()) {
743 switch (i.customId) {
744 case "back": {
745 closed = true;
746 break;
747 }
748 }
749 } else {
750 switch (i.customId) {
751 case "toAdd": {
752 let channelEmbed = new EmojiEmbed()
753 .setTitle("Clean Settings")
754 .setDescription(`Editing <#${i.values[0]}>`)
755 .setEmoji("GUILD.SETTINGS.GREEN")
756 .setStatus("Success")
757 let channelButtons = new ActionRowBuilder<ButtonBuilder>()
758 .addComponents(
759 new ButtonBuilder()
760 .setCustomId("back")
761 .setLabel("Back")
762 .setStyle(ButtonStyle.Primary)
763 .setEmoji(getEmojiByName("CONTROL.LEFT", "id")),
764 new ButtonBuilder()
765 .setCustomId("switch")
766 .setLabel(current.channels.includes(i.values[0]!) ? "Remove" : "Add")
767 .setStyle(current.channels.includes(i.values[0]!) ? ButtonStyle.Danger : ButtonStyle.Success)
768 )
769
770 await i.editReply({embeds: [channelEmbed], components: [channelButtons]});
771 let j: ButtonInteraction;
772 try {
773 j = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction;
774 } catch (e) {
775 closed = true;
776 break;
777 }
778 await j.deferUpdate();
779 switch (j.customId) {
780 case "back": {
781 break;
782 }
783 case "switch": {
784 if(current.channels.includes(i.values[0]!)) {
785 current.channels.splice(current.channels.indexOf(i.values[0]!), 1);
786 } else {
787 current.channels.push(i.values[0]!);
788 }
789 }
790 }
791 break;
792 }
793 case "allowed": {
794 switch (i.values[0]) {
795 case "users": {
796 current.allowed.user = await toSelectMenu(interaction, m, current.allowed.user, "member", "Mention Settings");
797 break;
798 }
799 case "roles": {
800 current.allowed.roles = await toSelectMenu(interaction, m, current.allowed.roles, "role", "Mention Settings");
801 break;
802 }
803 }
804 break;
805 }
806 }
807 }
808
809 } while(!closed);
810
811 return current;
812
813}
814
TheCodedProf8b3da212023-02-02 15:09:55 -0500815const callback = async (interaction: CommandInteraction): Promise<void> => {
816 if (!interaction.guild) return;
817 const m = await interaction.reply({embeds: LoadingEmbed, fetchReply: true, ephemeral: true});
818 const config = (await client.database.guilds.read(interaction.guild.id)).filters;
819
820 let closed = false;
821
822 const button = new ActionRowBuilder<ButtonBuilder>()
823 .addComponents(
824 new ButtonBuilder()
825 .setCustomId("save")
826 .setLabel("Save")
827 .setStyle(ButtonStyle.Success)
828 )
829
830 do {
831
832 const selectMenu = new ActionRowBuilder<StringSelectMenuBuilder>()
833 .addComponents(
834 new StringSelectMenuBuilder()
835 .setCustomId("filter")
836 .setPlaceholder("Select a filter to edit")
837 .addOptions(
838 new StringSelectMenuOptionBuilder()
839 .setLabel("Invites")
840 .setDescription("Automatically delete messages containing server invites")
841 .setValue("invites"),
842 new StringSelectMenuOptionBuilder()
843 .setLabel("Mentions")
844 .setDescription("Deletes messages with excessive mentions")
845 .setValue("mentions"),
846 new StringSelectMenuOptionBuilder()
847 .setLabel("Words")
848 .setDescription("Delete messages containing filtered words")
849 .setValue("words"),
850 new StringSelectMenuOptionBuilder()
851 .setLabel("Malware")
852 .setDescription("Automatically delete files and links containing malware")
853 .setValue("malware"),
854 new StringSelectMenuOptionBuilder()
855 .setLabel("Images")
856 .setDescription("Checks performed on images (NSFW, size checking, etc.)")
TheCodedProfad0b8202023-02-14 14:27:09 -0500857 .setValue("images"),
858 new StringSelectMenuOptionBuilder()
859 .setLabel("Clean")
860 .setDescription("Automatically delete new messages in specific channels")
861 .setValue("clean")
TheCodedProf8b3da212023-02-02 15:09:55 -0500862 )
863 );
864
865 const embed = new EmojiEmbed()
866 .setTitle("Automod Settings")
867 .setDescription(
TheCodedProf486bca32023-02-02 16:49:44 -0500868 `${emojiFromBoolean(config.invite.enabled)} **Invites**\n` +
TheCodedProf8b3da212023-02-02 15:09:55 -0500869 `${emojiFromBoolean(config.pings.everyone || config.pings.mass > 0 || config.pings.roles)} **Mentions**\n` +
870 `${emojiFromBoolean(config.wordFilter.enabled)} **Words**\n` +
871 `${emojiFromBoolean(config.malware)} **Malware**\n` +
TheCodedProfad0b8202023-02-14 14:27:09 -0500872 `${emojiFromBoolean(config.images.NSFW || config.images.size)} **Images**\n` +
873 `${emojiFromBoolean(config.clean.channels.length > 0)} **Clean**\n`
TheCodedProf8b3da212023-02-02 15:09:55 -0500874 )
TheCodedProf486bca32023-02-02 16:49:44 -0500875 .setStatus("Success")
876 .setEmoji("GUILD.SETTINGS.GREEN")
TheCodedProf8b3da212023-02-02 15:09:55 -0500877
878
879 await interaction.editReply({embeds: [embed], components: [selectMenu, button]});
880
881 let i: StringSelectMenuInteraction | ButtonInteraction;
882 try {
883 i = await m.awaitMessageComponent({filter: (i) => i.user.id === interaction.user.id && i.message.id === m.id, time: 300000}) as StringSelectMenuInteraction | ButtonInteraction;
884 } catch (e) {
885 closed = true;
PineaFanb0d0c242023-02-05 10:59:45 +0000886 continue;
TheCodedProf8b3da212023-02-02 15:09:55 -0500887 }
TheCodedProfad0b8202023-02-14 14:27:09 -0500888 await i.deferUpdate();
TheCodedProf8b3da212023-02-02 15:09:55 -0500889 if(i.isButton()) {
TheCodedProf8b3da212023-02-02 15:09:55 -0500890 await client.database.guilds.write(interaction.guild.id, {filters: config});
891 } else {
892 switch(i.values[0]) {
PineaFanb0d0c242023-02-05 10:59:45 +0000893 case "invites": {
TheCodedProf486bca32023-02-02 16:49:44 -0500894 config.invite = await inviteMenu(i, m, config.invite);
TheCodedProf8b3da212023-02-02 15:09:55 -0500895 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000896 }
897 case "mentions": {
TheCodedProf486bca32023-02-02 16:49:44 -0500898 config.pings = await mentionMenu(i, m, config.pings);
TheCodedProf8b3da212023-02-02 15:09:55 -0500899 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000900 }
901 case "words": {
TheCodedProf486bca32023-02-02 16:49:44 -0500902 config.wordFilter = await wordMenu(i, m, config.wordFilter);
TheCodedProf8b3da212023-02-02 15:09:55 -0500903 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000904 }
905 case "malware": {
TheCodedProf8b3da212023-02-02 15:09:55 -0500906 config.malware = !config.malware;
907 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000908 }
909 case "images": {
910 const next = await imageMenu(i, m, config.images);
911 config.images = next;
TheCodedProf8b3da212023-02-02 15:09:55 -0500912 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000913 }
TheCodedProfad0b8202023-02-14 14:27:09 -0500914 case "clean": {
915 const next = await cleanMenu(i, m, config.clean);
916 config.clean = next;
917 break;
918 }
TheCodedProf8b3da212023-02-02 15:09:55 -0500919 }
920 }
921
922 } while(!closed)
923
924};
925
926const check = (interaction: CommandInteraction, _partial: boolean = false) => {
927 const member = interaction.member as Discord.GuildMember;
928 if (!member.permissions.has("ManageMessages"))
929 return "You must have the *Manage Messages* permission to use this command";
930 return true;
931};
932
933export { command };
934export { callback };
935export { check };