/[sudobot]/branches/7.x/src/services/ReportService.ts
ViewVC logotype

Contents of /branches/7.x/src/services/ReportService.ts

Parent Directory Parent Directory | Revision Log Revision Log


Revision 577 - (show annotations)
Mon Jul 29 18:52:37 2024 UTC (8 months ago) by rakinar2
File MIME type: application/typescript
File size: 16963 byte(s)
chore: add old version archive branches (2.x to 9.x-dev)
1 /**
2 * This file is part of SudoBot.
3 *
4 * Copyright (C) 2021-2023 OSN Developers.
5 *
6 * SudoBot is free software; you can redistribute it and/or modify it
7 * under the terms of the GNU Affero General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * SudoBot is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU Affero General Public License for more details.
15 *
16 * You should have received a copy of the GNU Affero General Public License
17 * along with SudoBot. If not, see <https://www.gnu.org/licenses/>.
18 */
19
20 import {
21 APIEmbed,
22 ActionRowBuilder,
23 Colors,
24 Guild,
25 GuildMember,
26 Interaction,
27 Message,
28 MessageCreateOptions,
29 ModalBuilder,
30 ModalSubmitInteraction,
31 PermissionsBitField,
32 PermissionsString,
33 Snowflake,
34 StringSelectMenuBuilder,
35 StringSelectMenuInteraction,
36 TextBasedChannel,
37 TextInputBuilder,
38 TextInputStyle,
39 User
40 } from "discord.js";
41 import Service from "../core/Service";
42 import { GatewayEventListener } from "../decorators/GatewayEventListener";
43 import { HasEventListeners } from "../types/HasEventListeners";
44 import LevelBasedPermissionManager from "../utils/LevelBasedPermissionManager";
45 import { stringToTimeInterval } from "../utils/datetime";
46 import { userInfo } from "../utils/embed";
47 import { safeChannelFetch, safeMemberFetch } from "../utils/fetch";
48 import { logError } from "../utils/logger";
49 import { TODO } from "../utils/utils";
50
51 export const name = "reportService";
52
53 type ReportOptions = {
54 reason?: string;
55 moderator: GuildMember;
56 guildId: string;
57 message?: Message;
58 member?: GuildMember;
59 };
60
61 type Action = "ignore" | "warn" | "mute" | "kick" | "ban";
62
63 type ActionOptions = {
64 action: Action;
65 type: "m" | "u";
66 duration?: number;
67 reason: string;
68 member: GuildMember;
69 moderator: User;
70 guild: Guild;
71 };
72
73 export default class ReportService extends Service implements HasEventListeners {
74 protected readonly actionPastParticiples: Record<Action, string> = {
75 ban: "Banned",
76 ignore: "Ignored",
77 kick: "Kicked",
78 mute: "Muted",
79 warn: "Warned"
80 };
81
82 async check(guildId: string, moderator: GuildMember, member: GuildMember) {
83 const config = this.client.configManager.config[guildId!]?.message_reporting;
84 const manager = await this.client.permissionManager.getManager(guildId!);
85
86 if (!config?.enabled) {
87 return {
88 error: "Message reporting is not enabled in this server."
89 };
90 }
91
92 if (
93 (config.permission_level !== undefined &&
94 config.permission_level > 0 &&
95 this.client.permissionManager.usesLevelBasedMode(guildId!) &&
96 manager instanceof LevelBasedPermissionManager &&
97 manager.getPermissionLevel(moderator) < config.permission_level) ||
98 !manager.getMemberPermissions(moderator).permissions.has(config.permissions as PermissionsString[], true) ||
99 !moderator?.roles.cache.hasAll(...(config?.roles ?? []))
100 ) {
101 return {
102 error: "You don't have permission to report messages."
103 };
104 }
105
106 if (!this.client.permissionManager.shouldModerate(member!, moderator)) {
107 return {
108 error: "You're missing permissions to moderate this user!"
109 };
110 }
111
112 return {
113 error: null
114 };
115 }
116
117 async report({ reason, moderator, guildId, member, message }: ReportOptions) {
118 const config = this.client.configManager.config[guildId!]?.message_reporting;
119 const { error } = await this.check(guildId, moderator, member ?? message?.member!);
120
121 if (error) {
122 return { error };
123 }
124
125 const guild = this.client.guilds.cache.get(guildId)!;
126
127 if (config?.logging_channel) {
128 const channel = await safeChannelFetch(guild, config?.logging_channel);
129
130 if (channel?.isTextBased()) {
131 await this.sendReportLog(
132 channel,
133 (message?.author ?? member?.user)!,
134 member ? "m" : "u",
135 {
136 title: `${member ? "User" : "Message"} Reported`,
137 description: message?.content,
138 fields: [
139 {
140 name: "User",
141 value: userInfo((message?.author ?? member?.user)!),
142 inline: true
143 },
144 {
145 name: "Responsible Moderator",
146 value: userInfo(moderator.user),
147 inline: true
148 },
149 {
150 name: "Reason",
151 value: reason ?? "*No reason provided*"
152 }
153 ],
154 footer: {
155 text: `${member ? "This user was muted for 3 hours" : "Reported"}` // TODO
156 }
157 },
158 {
159 files: message
160 ? message.attachments.map(a => ({
161 attachment: a.proxyURL,
162 name: a.name,
163 description: a.description ?? undefined
164 }))
165 : undefined
166 }
167 );
168 }
169 }
170
171 if (member) {
172 TODO("Reporting members is not supported yet");
173 } else if (message && message.deletable) {
174 message.delete().catch(logError);
175 }
176
177 return {
178 success: true
179 };
180 }
181
182 sendReportLog(
183 channel: TextBasedChannel,
184 offender: User,
185 type: "m" | "u",
186 embedOptions?: APIEmbed,
187 messageOptions?: MessageCreateOptions
188 ) {
189 return channel.send({
190 embeds: [
191 {
192 author: {
193 name: offender.username,
194 icon_url: offender.displayAvatarURL()
195 },
196 color: 0x007bff,
197 timestamp: new Date().toISOString(),
198 ...(embedOptions ?? {})
199 }
200 ],
201 components: [
202 new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
203 new StringSelectMenuBuilder()
204 .setCustomId(`report_action_${type}_${offender.id}`)
205 .setMinValues(0)
206 .setMaxValues(1)
207 .setPlaceholder("Select an action to take...")
208 .setOptions(
209 {
210 label: "Ignore",
211 description: "Ignore the report and take no action",
212 value: "ignore",
213 emoji: "✅"
214 },
215 {
216 label: "Warn",
217 description: "Warn the user regarding this report",
218 value: "warn",
219 emoji: "🛡️"
220 },
221 {
222 label: "Mute",
223 description: "Mutes the user",
224 value: "mute",
225 emoji: "⌚"
226 },
227 {
228 label: "Kick",
229 description: "Kicks the user from the server",
230 value: "kick",
231 emoji: "🔨"
232 },
233 {
234 label: "Ban",
235 description: "Bans the user from the server",
236 value: "ban",
237 emoji: "⚙"
238 }
239 )
240 )
241 ],
242 ...(messageOptions ?? {})
243 });
244 }
245
246 permissionCheck(action: Action, guildId: string, memberPermissions: PermissionsBitField) {
247 const config = this.client.configManager.config[guildId!]?.message_reporting;
248 const requiredPermissions = config?.action_required_permissions[action] as [
249 PermissionsString | "or",
250 ...PermissionsString[]
251 ];
252
253 if (!requiredPermissions) {
254 return false;
255 }
256
257 const mode = requiredPermissions[0] === "or" ? "or" : "and";
258
259 if (mode === "or") {
260 requiredPermissions.shift();
261 }
262
263 return memberPermissions[mode === "or" ? "any" : "has"](requiredPermissions as PermissionsString[], true);
264 }
265
266 async onStringSelectMenuInteraction(interaction: StringSelectMenuInteraction) {
267 if (!interaction.values.length) {
268 await interaction.deferUpdate();
269 return;
270 }
271
272 const action = interaction.values[0] as Action;
273 const [type, userId] = interaction.customId.split("_").slice(2);
274
275 if (!(await this.commonChecks(action as Action, userId, interaction))) {
276 return;
277 }
278
279 if (action === "ignore") {
280 await this.editMessage(interaction, "Ignored");
281
282 await interaction.reply({
283 content: "Operation completed.",
284 ephemeral: true
285 });
286
287 return;
288 }
289
290 const modal = new ModalBuilder()
291 .setCustomId(`report_action_info_${action}_${type}_${userId}`)
292 .setTitle(`${action[0].toUpperCase()}${action.substring(1)} Member`)
293 .addComponents(
294 new ActionRowBuilder<TextInputBuilder>().addComponents(
295 new TextInputBuilder()
296 .setCustomId("reason")
297 .setLabel("Reason")
298 .setPlaceholder("Type a reason for this action here...")
299 .setStyle(TextInputStyle.Paragraph)
300 )
301 );
302
303 if (action === "ban" || action === "mute") {
304 modal.addComponents(
305 new ActionRowBuilder<TextInputBuilder>().addComponents(
306 new TextInputBuilder()
307 .setCustomId("duration")
308 .setLabel("Duration")
309 .setPlaceholder("e.g. (10d or 24h)")
310 .setStyle(TextInputStyle.Short)
311 .setRequired(false)
312 )
313 );
314 }
315
316 await interaction.showModal(modal);
317 }
318
319 editMessage(interaction: StringSelectMenuInteraction | ModalSubmitInteraction, action: string, color?: number) {
320 return interaction.message?.edit({
321 components: [],
322 embeds: [
323 {
324 ...interaction.message.embeds[0].data,
325 fields: [
326 ...interaction.message.embeds[0].fields,
327 {
328 name: "Action",
329 value: action,
330 inline: true
331 },
332 {
333 name: "Action Taken By",
334 value: userInfo(interaction.user),
335 inline: true
336 }
337 ],
338 color: color ?? Colors.Green
339 }
340 ]
341 });
342 }
343
344 async commonChecks(action: Action, userId: string, interaction: ModalSubmitInteraction | StringSelectMenuInteraction) {
345 if (
346 !interaction.memberPermissions ||
347 !this.permissionCheck(action, interaction.guildId!, interaction.memberPermissions)
348 ) {
349 await interaction.reply({
350 ephemeral: true,
351 content: "You don't have permission to take action on reports."
352 });
353
354 return false;
355 }
356
357 const member = await safeMemberFetch(interaction.guild!, userId);
358
359 if (!member) {
360 await interaction.reply({
361 content: "The member is no longer in the server!",
362 ephemeral: true
363 });
364
365 return false;
366 }
367
368 const { error } = await this.check(interaction.guildId!, interaction.member! as GuildMember, member);
369
370 if (error) {
371 await interaction.reply({
372 content: error,
373 ephemeral: true
374 });
375
376 return false;
377 }
378
379 return true;
380 }
381
382 async onModalSubmit(interaction: ModalSubmitInteraction) {
383 const [action, type, userId] = interaction.customId.split("_").slice(3) as [Action, "m" | "u", Snowflake];
384
385 if (!(await this.commonChecks(action, userId, interaction))) {
386 return;
387 }
388
389 const duration = interaction.fields.fields.find(field => field.customId === "duration")
390 ? interaction.fields.getTextInputValue("duration")
391 : null;
392 const reason = interaction.fields.getTextInputValue("reason");
393 const parsedDuration = duration
394 ? stringToTimeInterval(duration, {
395 milliseconds: true
396 })
397 : null;
398
399 if (parsedDuration && parsedDuration.error) {
400 await interaction.reply({
401 ephemeral: true,
402 content: "Invalid duration given. The duration should look something like these: 20h, 50m, 10m60s etc."
403 });
404
405 return;
406 }
407
408 await interaction.deferReply({
409 ephemeral: true
410 });
411
412 const member = await safeMemberFetch(interaction.guild!, userId);
413
414 if (!member) {
415 await interaction.editReply({
416 content: "Failed to find the member in the server!"
417 });
418
419 return;
420 }
421
422 await this.takeAction({
423 action,
424 type,
425 member,
426 reason,
427 duration: parsedDuration?.result,
428 moderator: interaction.user,
429 guild: interaction.guild!
430 });
431
432 await this.editMessage(interaction, this.actionPastParticiples[action], Colors.Red);
433 await interaction.editReply({
434 content: "Operation completed."
435 });
436 }
437
438 takeAction({ action, member, guild, moderator, reason, duration }: ActionOptions) {
439 switch (action) {
440 case "ban":
441 return this.client.infractionManager.createUserBan(member.user, {
442 guild,
443 moderator,
444 autoRemoveQueue: true,
445 duration,
446 notifyUser: true,
447 reason,
448 sendLog: true
449 });
450
451 case "kick":
452 return this.client.infractionManager.createMemberKick(member, {
453 guild,
454 moderator,
455 notifyUser: true,
456 reason,
457 sendLog: true
458 });
459
460 case "mute":
461 return this.client.infractionManager.createMemberMute(member, {
462 guild,
463 moderator,
464 notifyUser: true,
465 reason,
466 sendLog: true,
467 autoRemoveQueue: true,
468 duration
469 });
470
471 case "ignore":
472 return null;
473
474 case "warn":
475 return this.client.infractionManager.createMemberWarn(member, {
476 guild,
477 moderator,
478 notifyUser: true,
479 reason,
480 sendLog: true
481 });
482 }
483 }
484
485 @GatewayEventListener("interactionCreate")
486 async onInteractionCreate(interaction: Interaction) {
487 if (interaction.isStringSelectMenu() && interaction.customId.startsWith("report_action_")) {
488 await this.onStringSelectMenuInteraction(interaction);
489 return;
490 }
491
492 if (interaction.isModalSubmit() && interaction.customId.startsWith(`report_action_info_`)) {
493 await this.onModalSubmit(interaction);
494 return;
495 }
496 }
497 }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26