blob: a7b6dbc908e0c1589c6701893ac6b7101eb82887 [file] [log] [blame]
TheCodedProf8b3da212023-02-02 15:09:55 -05001import type Discord from "discord.js";
TheCodedProf486bca32023-02-02 16:49:44 -05002import { ActionRowBuilder, AnyComponent, AnyComponentBuilder, AnySelectMenuInteraction, APIMessageComponentEmoji, ButtonBuilder, ButtonInteraction, ButtonStyle, ChannelSelectMenuBuilder, ChannelSelectMenuInteraction, CommandInteraction, Guild, GuildMember, GuildTextBasedChannel, MentionableSelectMenuBuilder, Message, Role, RoleSelectMenuBuilder, RoleSelectMenuInteraction, SelectMenuBuilder, StringSelectMenuBuilder, StringSelectMenuInteraction, StringSelectMenuOptionBuilder, UserSelectMenuBuilder, UserSelectMenuInteraction } from "discord.js";
TheCodedProf8b3da212023-02-02 15:09:55 -05003import type { SlashCommandSubcommandBuilder } from "discord.js";
4import { LoadingEmbed } from "../../utils/defaults.js";
5import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
6import client from "../../utils/client.js";
7import getEmojiByName from "../../utils/getEmojiByName.js";
8
TheCodedProf486bca32023-02-02 16:49:44 -05009
TheCodedProf8b3da212023-02-02 15:09:55 -050010const command = (builder: SlashCommandSubcommandBuilder) =>
11 builder.setName("automod").setDescription("Setting for automatic moderation features");
12
13
14const emojiFromBoolean = (bool: boolean, id?: string) => bool ? getEmojiByName("CONTROL.TICK", id) : getEmojiByName("CONTROL.CROSS", id);
15
TheCodedProf486bca32023-02-02 16:49:44 -050016const listToAndMore = (list: string[], max: number) => {
17 // PineappleFan, Coded, Mini (and 10 more)
18 if(list.length > max) {
19 return list.slice(0, max).join(", ") + ` (and ${list.length - max} more)`;
20 }
21 return list.join(", ");
22}
23
24const toSelectMenu = async (interaction: StringSelectMenuInteraction, m: Message, ids: string[], type: "member" | "role" | "channel", title: string): Promise<string[]> => {
25
26 const back = new ActionRowBuilder<ButtonBuilder>().addComponents(new ButtonBuilder().setCustomId("back").setLabel("Back").setStyle(ButtonStyle.Secondary).setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji));
27
28 let closed;
29 do {
30 let render: string[] = []
31 let mapped: string[] = [];
32 let menu: UserSelectMenuBuilder | RoleSelectMenuBuilder | ChannelSelectMenuBuilder;
33 switch(type) {
34 case "member":
35 menu = new UserSelectMenuBuilder().setCustomId("user").setPlaceholder("Select users").setMaxValues(25);
36 mapped = await Promise.all(ids.map(async (id) => { return (await client.users.fetch(id).then(user => user.tag)) || "Unknown User" }));
37 render = ids.map(id => client.logger.renderUser(id))
38 break;
39 case "role":
40 menu = new RoleSelectMenuBuilder().setCustomId("role").setPlaceholder("Select roles").setMaxValues(25);
41 mapped = await Promise.all(ids.map(async (id) => { return (await interaction.guild!.roles.fetch(id).then(role => role?.name)) || "Unknown Role" }));
42 render = ids.map(id => client.logger.renderRole(id, interaction.guild!))
43 break;
44 case "channel":
45 menu = new ChannelSelectMenuBuilder().setCustomId("channel").setPlaceholder("Select channels").setMaxValues(25);
46 mapped = await Promise.all(ids.map(async (id) => { return (await interaction.guild!.channels.fetch(id).then(channel => channel?.name)) || "Unknown Channel" }));
47 render = ids.map(id => client.logger.renderChannel(id))
48 break;
49 }
50 const removeOptions = new ActionRowBuilder<StringSelectMenuBuilder>()
51 .addComponents(
52 new StringSelectMenuBuilder()
53 .setCustomId("remove")
54 .setPlaceholder("Remove")
55 .addOptions(mapped.map((name, i) => new StringSelectMenuOptionBuilder().setLabel(name).setValue(ids[i]!)))
56 .setDisabled(ids.length === 0)
57 );
58
59 const embed = new EmojiEmbed()
60 .setTitle(title)
61 .setEmoji(getEmojiByName("GUILD.SETTINGS.GREEN"))
62 .setDescription(`Select ${type}s:\n\nCurrent:\n` + (render.length > 0 ? render.join("\n") : "None"))
63 .setStatus("Success");
64 let components: ActionRowBuilder<
65 StringSelectMenuBuilder |
66 ButtonBuilder |
67 ChannelSelectMenuBuilder |
68 UserSelectMenuBuilder |
69 RoleSelectMenuBuilder
70 >[] = [new ActionRowBuilder<typeof menu>().addComponents(menu)]
71 if(ids.length > 0) components.push(removeOptions);
72 components.push(back);
73
74 await interaction.editReply({embeds: [embed], components: components})
75
76 let i: AnySelectMenuInteraction | ButtonInteraction;
77 try {
78 i = await interaction.channel!.awaitMessageComponent({filter: i => i.user.id === interaction.user.id, time: 300000});
79 } catch(e) {
80 closed = true;
81 break;
82 }
83
84 if(i.isButton()) {
85 await i.deferUpdate();
86 if(i.customId === "back") {
87 closed = true;
88 break;
89 }
90 } else if(i.isStringSelectMenu()) {
91 await i.deferUpdate();
92 if(i.customId === "remove") {
93 ids = ids.filter(id => id !== (i as StringSelectMenuInteraction).values[0]);
94 if(ids.length === 0) {
95 menu.data.disabled = true;
96 }
97 }
98 } else {
99 await i.deferUpdate();
100 if(i.customId === "user") {
101 ids = ids.concat((i as UserSelectMenuInteraction).values);
102 } else if(i.customId === "role") {
103 ids = ids.concat((i as RoleSelectMenuInteraction).values);
104 } else if(i.customId === "channel") {
105 ids = ids.concat((i as ChannelSelectMenuInteraction).values);
106 }
107 }
108
109 } while(!closed)
110 return ids;
111}
TheCodedProf8b3da212023-02-02 15:09:55 -0500112
113const imageMenu = async (interaction: StringSelectMenuInteraction, m: Message, current: {
114 NSFW: boolean,
115 size: boolean
116}): Promise<{NSFW: boolean, size: boolean}> => {
117 let closed = false;
118 do {
119 const options = new ActionRowBuilder<ButtonBuilder>()
120 .addComponents(
121 new ButtonBuilder()
122 .setCustomId("back")
123 .setLabel("Back")
124 .setStyle(ButtonStyle.Secondary)
125 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
126 new ButtonBuilder()
127 .setCustomId("nsfw")
128 .setLabel("NSFW")
129 .setStyle(current.NSFW ? ButtonStyle.Success : ButtonStyle.Danger)
130 .setEmoji(emojiFromBoolean(current.NSFW, "id") as APIMessageComponentEmoji),
131 new ButtonBuilder()
132 .setCustomId("size")
133 .setLabel("Size")
134 .setStyle(current.size ? ButtonStyle.Success : ButtonStyle.Danger)
135 .setEmoji(emojiFromBoolean(current.size, "id") as APIMessageComponentEmoji)
136 )
137
138 const embed = new EmojiEmbed()
139 .setTitle("Image Settings")
140 .setDescription(
141 `${emojiFromBoolean(current.NSFW)} **NSFW**\n` +
142 `${emojiFromBoolean(current.size)} **Size**\n`
143 )
144
145 await interaction.editReply({embeds: [embed], components: [options]});
146
147 let i: ButtonInteraction;
148 try {
149 i = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction;
150 } catch (e) {
151 return current;
152 }
153 await i.deferUpdate();
154 switch(i.customId) {
155 case "back":
156 closed = true;
157 break;
158 case "nsfw":
159 current.NSFW = !current.NSFW;
160 break;
161 case "size":
162 current.size = !current.size;
163 break;
164 }
165 } while(!closed);
166 return current;
167}
168
169const wordMenu = async (interaction: StringSelectMenuInteraction, m: Message, current: {
170 enabled: boolean,
171 words: {strict: string[], loose: string[]},
TheCodedProf486bca32023-02-02 16:49:44 -0500172 allowed: {users: string[], roles: string[], channels: string[]}
TheCodedProf8b3da212023-02-02 15:09:55 -0500173}): Promise<{
174 enabled: boolean,
175 words: {strict: string[], loose: string[]},
TheCodedProf486bca32023-02-02 16:49:44 -0500176 allowed: {users: string[], roles: string[], channels: string[]}
TheCodedProf8b3da212023-02-02 15:09:55 -0500177}> => {
TheCodedProf486bca32023-02-02 16:49:44 -0500178 let closed = false;
179 do {
180 closed = true;
181 } while(!closed);
182 return current;
TheCodedProf8b3da212023-02-02 15:09:55 -0500183}
184
185const inviteMenu = async (interaction: StringSelectMenuInteraction, m: Message, current: {
186 enabled: boolean,
TheCodedProf486bca32023-02-02 16:49:44 -0500187 allowed: {users: string[], roles: string[], channels: string[]}
TheCodedProf8b3da212023-02-02 15:09:55 -0500188}): Promise<{
189 enabled: boolean,
TheCodedProf486bca32023-02-02 16:49:44 -0500190 allowed: {users: string[], roles: string[], channels: string[]}
TheCodedProf8b3da212023-02-02 15:09:55 -0500191}> => {
192
TheCodedProf486bca32023-02-02 16:49:44 -0500193 let closed = false;
194 do {
195 const buttons = new ActionRowBuilder<ButtonBuilder>()
196 .addComponents(
197 new ButtonBuilder()
198 .setCustomId("back")
199 .setLabel("Back")
200 .setStyle(ButtonStyle.Secondary)
201 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
202 new ButtonBuilder()
203 .setCustomId("enabled")
204 .setLabel(current.enabled ? "Enabled" : "Disabled")
205 .setStyle(current.enabled ? ButtonStyle.Success : ButtonStyle.Danger)
206 .setEmoji(emojiFromBoolean(current.enabled, "id") as APIMessageComponentEmoji)
207 );
208 const menu = new ActionRowBuilder<StringSelectMenuBuilder>()
209 .addComponents(
210 new StringSelectMenuBuilder()
211 .setCustomId("toEdit")
212 .setPlaceholder("Edit your allow list")
213 .addOptions(
214 new StringSelectMenuOptionBuilder()
215 .setLabel("Users")
216 .setDescription("Users that are allowed to send invites")
217 .setValue("users"),
218 new StringSelectMenuOptionBuilder()
219 .setLabel("Roles")
220 .setDescription("Roles that are allowed to send invites")
221 .setValue("roles"),
222 new StringSelectMenuOptionBuilder()
223 .setLabel("Channels")
224 .setDescription("Channels that anyone is allowed to send invites in")
225 .setValue("channels")
226 ).setDisabled(!current.enabled)
227 )
228
229 const embed = new EmojiEmbed()
230 .setTitle("Invite Settings")
231 .setDescription(
232 "Automatically deletes invites sent by users (outside of staff members and self promotion channels)" + `\n\n` +
233 `${emojiFromBoolean(current.enabled)} **${current.enabled ? "Enabled" : "Disabled"}**\n\n` +
234 `**Users:** ` + listToAndMore(current.allowed.users.map(user => `> <@${user}>`), 5) + `\n` +
235 `**Roles:** ` + listToAndMore(current.allowed.roles.map(role => `> <@&${role}>`), 5) + `\n` +
236 `**Channels:** ` + listToAndMore(current.allowed.channels.map(channel => `> <#${channel}>`), 5)
237 )
238 .setStatus("Success")
239 .setEmoji("GUILD.SETTINGS.GREEN")
240
241
242 await interaction.editReply({embeds: [embed], components: [buttons, menu]});
243
244 let i: ButtonInteraction | StringSelectMenuInteraction;
245 try {
246 i = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction | StringSelectMenuInteraction;
247 } catch (e) {
248 return current;
249 }
250
251 if(i.isButton()) {
252 await i.deferUpdate();
253 switch(i.customId) {
254 case "back":
255 closed = true;
256 break;
257 case "enabled":
258 current.enabled = !current.enabled;
259 break;
260 }
261 } else {
262 await i.deferUpdate();
263 switch(i.values[0]) {
264 case "users":
265 current.allowed.users = await toSelectMenu(interaction, m, current.allowed.users, "member", "Invite Settings");
266 break;
267 case "roles":
268 current.allowed.roles = await toSelectMenu(interaction, m, current.allowed.roles, "role", "Invite Settings");
269 break;
270 case "channels":
271 current.allowed.channels = await toSelectMenu(interaction, m, current.allowed.channels, "channel", "Invite Settings");
272 break;
273 }
274 }
275
276 } while(!closed);
277 return current;
TheCodedProf8b3da212023-02-02 15:09:55 -0500278}
279
280const mentionMenu = async (interaction: StringSelectMenuInteraction, m: Message, current: {
281 mass: number,
282 everyone: boolean,
283 roles: boolean,
284 allowed: {
285 roles: string[],
286 rolesToMention: string[],
287 users: string[],
288 channels: string[]
289 }
290}): Promise<{
291 mass: number,
292 everyone: boolean,
293 roles: boolean,
294 allowed: {
295 roles: string[],
296 rolesToMention: string[],
297 users: string[],
298 channels: string[]
299 }
300}> => {
TheCodedProf486bca32023-02-02 16:49:44 -0500301 let closed = false;
302
303 do {
304 closed = true;
305 } while(!closed);
306 return current
TheCodedProf8b3da212023-02-02 15:09:55 -0500307}
308
309const callback = async (interaction: CommandInteraction): Promise<void> => {
310 if (!interaction.guild) return;
311 const m = await interaction.reply({embeds: LoadingEmbed, fetchReply: true, ephemeral: true});
312 const config = (await client.database.guilds.read(interaction.guild.id)).filters;
313
314 let closed = false;
315
316 const button = new ActionRowBuilder<ButtonBuilder>()
317 .addComponents(
318 new ButtonBuilder()
319 .setCustomId("save")
320 .setLabel("Save")
321 .setStyle(ButtonStyle.Success)
322 )
323
324 do {
325
326 const selectMenu = new ActionRowBuilder<StringSelectMenuBuilder>()
327 .addComponents(
328 new StringSelectMenuBuilder()
329 .setCustomId("filter")
330 .setPlaceholder("Select a filter to edit")
331 .addOptions(
332 new StringSelectMenuOptionBuilder()
333 .setLabel("Invites")
334 .setDescription("Automatically delete messages containing server invites")
335 .setValue("invites"),
336 new StringSelectMenuOptionBuilder()
337 .setLabel("Mentions")
338 .setDescription("Deletes messages with excessive mentions")
339 .setValue("mentions"),
340 new StringSelectMenuOptionBuilder()
341 .setLabel("Words")
342 .setDescription("Delete messages containing filtered words")
343 .setValue("words"),
344 new StringSelectMenuOptionBuilder()
345 .setLabel("Malware")
346 .setDescription("Automatically delete files and links containing malware")
347 .setValue("malware"),
348 new StringSelectMenuOptionBuilder()
349 .setLabel("Images")
350 .setDescription("Checks performed on images (NSFW, size checking, etc.)")
351 .setValue("images")
352 )
353 );
354
355 const embed = new EmojiEmbed()
356 .setTitle("Automod Settings")
357 .setDescription(
TheCodedProf486bca32023-02-02 16:49:44 -0500358 `${emojiFromBoolean(config.invite.enabled)} **Invites**\n` +
TheCodedProf8b3da212023-02-02 15:09:55 -0500359 `${emojiFromBoolean(config.pings.everyone || config.pings.mass > 0 || config.pings.roles)} **Mentions**\n` +
360 `${emojiFromBoolean(config.wordFilter.enabled)} **Words**\n` +
361 `${emojiFromBoolean(config.malware)} **Malware**\n` +
TheCodedProf486bca32023-02-02 16:49:44 -0500362 `${emojiFromBoolean(config.images.NSFW || config.images.size)} **Images**\n`
TheCodedProf8b3da212023-02-02 15:09:55 -0500363 )
TheCodedProf486bca32023-02-02 16:49:44 -0500364 .setStatus("Success")
365 .setEmoji("GUILD.SETTINGS.GREEN")
TheCodedProf8b3da212023-02-02 15:09:55 -0500366
367
368 await interaction.editReply({embeds: [embed], components: [selectMenu, button]});
369
370 let i: StringSelectMenuInteraction | ButtonInteraction;
371 try {
372 i = await m.awaitMessageComponent({filter: (i) => i.user.id === interaction.user.id && i.message.id === m.id, time: 300000}) as StringSelectMenuInteraction | ButtonInteraction;
373 } catch (e) {
374 closed = true;
375 return;
376 }
377 if(!i) return;
378 if(i.isButton()) {
379 await i.deferUpdate();
380 await client.database.guilds.write(interaction.guild.id, {filters: config});
381 } else {
382 switch(i.values[0]) {
383 case "invites":
TheCodedProf486bca32023-02-02 16:49:44 -0500384 await i.deferUpdate();
385 config.invite = await inviteMenu(i, m, config.invite);
TheCodedProf8b3da212023-02-02 15:09:55 -0500386 break;
387 case "mentions":
TheCodedProf486bca32023-02-02 16:49:44 -0500388 await i.deferUpdate();
389 config.pings = await mentionMenu(i, m, config.pings);
TheCodedProf8b3da212023-02-02 15:09:55 -0500390 break;
391 case "words":
TheCodedProf486bca32023-02-02 16:49:44 -0500392 await i.deferUpdate();
393 config.wordFilter = await wordMenu(i, m, config.wordFilter);
TheCodedProf8b3da212023-02-02 15:09:55 -0500394 break;
395 case "malware":
396 await i.deferUpdate();
397 config.malware = !config.malware;
398 break;
399 case "images":
400 let next = await imageMenu(i, m, config.images);
401 if(next) config.images = next;
402 break;
403 }
404 }
405
406 } while(!closed)
407
408};
409
410const check = (interaction: CommandInteraction, _partial: boolean = false) => {
411 const member = interaction.member as Discord.GuildMember;
412 if (!member.permissions.has("ManageMessages"))
413 return "You must have the *Manage Messages* permission to use this command";
414 return true;
415};
416
417export { command };
418export { callback };
419export { check };