/[sudobot]/branches/6.x/src/services/InfractionManager.ts
ViewVC logotype

Annotation of /branches/6.x/src/services/InfractionManager.ts

Parent Directory Parent Directory | Revision Log Revision Log


Revision 577 - (hide annotations)
Mon Jul 29 18:52:37 2024 UTC (8 months ago) by rakinar2
File MIME type: application/typescript
File size: 40741 byte(s)
chore: add old version archive branches (2.x to 9.x-dev)
1 rakinar2 577 /**
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 { Infraction, InfractionType } from "@prisma/client";
21     import { AlignmentEnum, AsciiTable3 } from "ascii-table3";
22     import { formatDistanceToNowStrict } from "date-fns";
23     import {
24     APIEmbed,
25     APIEmbedField,
26     APIUser,
27     ChannelType,
28     ColorResolvable,
29     DiscordAPIError,
30     EmbedBuilder,
31     EmbedField,
32     Guild,
33     GuildMember,
34     Message,
35     MessageResolvable,
36     OverwriteData,
37     PermissionFlagsBits,
38     PrivateThreadChannel,
39     TextBasedChannel,
40     TextChannel,
41     ThreadAutoArchiveDuration,
42     User,
43     escapeCodeBlock,
44     escapeMarkdown,
45     time
46     } from "discord.js";
47     import path from "path";
48     import Service from "../core/Service";
49     import QueueEntry from "../utils/QueueEntry";
50     import { safeChannelFetch } from "../utils/fetch";
51     import { log, logError } from "../utils/logger";
52     import { getEmoji, wait } from "../utils/utils";
53    
54     export const name = "infractionManager";
55    
56     export default class InfractionManager extends Service {
57     generateInfractionDetailsEmbed(user: User | null, infraction: Infraction) {
58     let metadataString = "";
59    
60     if (infraction.metadata && typeof infraction.metadata === "object") {
61     metadataString += "```\n";
62    
63     for (const [key, value] of Object.entries(infraction.metadata as Record<string, string>)) {
64     log(key, value);
65     metadataString += `${key}: ${escapeCodeBlock(`${value}`)}\n`;
66     }
67    
68     metadataString += "\n```";
69     }
70    
71     return new EmbedBuilder({
72     author: {
73     name: user?.username ?? "Unknown User",
74     iconURL: user?.displayAvatarURL() ?? undefined
75     },
76     color: 0x007bff,
77     fields: [
78     {
79     name: "ID",
80     value: infraction.id.toString(),
81     inline: true
82     },
83     {
84     name: "Action Type",
85     value:
86     infraction.type === InfractionType.BULK_DELETE_MESSAGE
87     ? "Bulk message delete"
88     : infraction.type[0] + infraction.type.substring(1).toLowerCase(),
89     inline: true
90     },
91     ...(infraction.queueId
92     ? [
93     {
94     name: "Associated Queue ID",
95     value: infraction.queueId.toString(),
96     inline: true
97     }
98     ]
99     : []),
100     {
101     name: "User",
102     value: `${user?.username ?? `<@${infraction.userId}>`} (${infraction.userId})`
103     },
104     {
105     name: "Responsible Moderator",
106     value: `<@${infraction.moderatorId}> (${infraction.moderatorId})`,
107     inline: true
108     },
109     {
110     name: "Metadata",
111     value: metadataString === "" ? "*No metadata available*" : metadataString
112     },
113     {
114     name: "Reason",
115     value: infraction.reason ?? "*No reason provided*"
116     },
117    
118     {
119     name: "Created At",
120     value: `${infraction.createdAt.toLocaleString()} (${time(infraction.createdAt)})`,
121     inline: true
122     },
123     {
124     name: "Updated At",
125     value: `${infraction.updatedAt.toLocaleString()} (${time(infraction.updatedAt)})`,
126     inline: true
127     },
128     ...(infraction.expiresAt
129     ? [
130     {
131     name: `Expire${infraction.expiresAt.getTime() <= Date.now() ? "d" : "s"} At`,
132     value: `${infraction.expiresAt.toLocaleString()} (${time(infraction.expiresAt)})`,
133     inline: true
134     }
135     ]
136     : [])
137     ]
138     }).setTimestamp();
139     }
140    
141     private async sendDMBuildEmbed(guild: Guild, options: SendDMOptions) {
142     const { fields, description, actionDoneName, id, reason, color, title } = options;
143    
144     const internalFields: EmbedField[] = [
145     ...(this.client.configManager.config[guild.id]!.infractions?.send_ids_to_user
146     ? [
147     {
148     name: "Infraction ID",
149     value: `${id}`,
150     inline: false
151     }
152     ]
153     : [])
154     ];
155    
156     return new EmbedBuilder({
157     author: {
158     name: actionDoneName ? `You have been ${actionDoneName} in ${guild.name}` : title ?? "",
159     iconURL: guild.iconURL() ?? undefined
160     },
161     description,
162     fields: [
163     {
164     name: "Reason",
165     value: reason ?? "*No reason provided*"
166     },
167     ...(fields ? (typeof fields === "function" ? await fields(internalFields) : fields) : []),
168     ...(typeof fields === "function" ? [] : internalFields ?? [])
169     ]
170     })
171     .setTimestamp()
172     .setColor(color ?? 0x0f14a60);
173     }
174    
175     private async sendDM(user: User, guild: Guild, options: SendDMOptions) {
176     const embed = await this.sendDMBuildEmbed(guild, options);
177     const { fallback = false, infraction } = options;
178    
179     try {
180     await user.send({
181     embeds: [embed]
182     });
183    
184     return true;
185     } catch (e) {
186     logError(e);
187    
188     if (!fallback || !infraction) {
189     return false;
190     }
191    
192     try {
193     return await this.notifyUserFallback({ infraction, user, guild, sendDMOptions: options, embed });
194     } catch (e) {
195     logError(e);
196     return false;
197     }
198     }
199     }
200    
201     async createUserSoftban(
202     user: User,
203     {
204     guild,
205     moderator,
206     reason,
207     deleteMessageSeconds,
208     notifyUser,
209     sendLog
210     }: CreateUserBanOptions & { deleteMessageSeconds: number }
211     ) {
212     const { id } = await this.client.prisma.infraction.create({
213     data: {
214     type: InfractionType.SOFTBAN,
215     userId: user.id,
216     guildId: guild.id,
217     reason,
218     moderatorId: moderator.id,
219     metadata: {
220     deleteMessageSeconds
221     }
222     }
223     });
224    
225     if (sendLog)
226     this.client.logger.logUserSoftBan({
227     moderator,
228     guild,
229     id: `${id}`,
230     user,
231     deleteMessageSeconds,
232     reason
233     });
234    
235     if (notifyUser) {
236     await this.sendDM(user, guild, {
237     id,
238     actionDoneName: "softbanned",
239     reason
240     });
241     }
242    
243     try {
244     await guild.bans.create(user, {
245     reason,
246     deleteMessageSeconds
247     });
248    
249     log("Seconds: " + deleteMessageSeconds);
250    
251     await wait(1500);
252     await guild.bans.remove(user, `Softban remove: ${reason}`);
253     return id;
254     } catch (e) {
255     logError(e);
256     return null;
257     }
258     }
259    
260     async createUserBan(
261     user: User,
262     { guild, moderator, reason, deleteMessageSeconds, notifyUser, duration, sendLog, autoRemoveQueue }: CreateUserBanOptions
263     ) {
264     const { id } = await this.client.prisma.infraction.create({
265     data: {
266     type: duration ? InfractionType.TEMPBAN : InfractionType.BAN,
267     userId: user.id,
268     guildId: guild.id,
269     reason,
270     moderatorId: moderator.id,
271     metadata: {
272     deleteMessageSeconds,
273     duration
274     }
275     }
276     });
277    
278     if (autoRemoveQueue) {
279     log("Auto remove", this.client.queueManager.queues);
280     await this.autoRemoveUnbanQueue(guild, user).catch(logError);
281     }
282    
283     if (sendLog)
284     this.client.logger.logUserBan({
285     moderator,
286     guild,
287     id: `${id}`,
288     user,
289     deleteMessageSeconds,
290     reason,
291     duration
292     });
293    
294     if (notifyUser) {
295     await this.sendDM(user, guild, {
296     id,
297     actionDoneName: "banned",
298     reason,
299     fields: duration
300     ? [
301     {
302     name: "Duration",
303     value: formatDistanceToNowStrict(new Date(Date.now() - duration))
304     }
305     ]
306     : undefined
307     }).catch(logError);
308     }
309    
310     try {
311     await guild.bans.create(user, {
312     reason,
313     deleteMessageSeconds
314     });
315    
316     log("Seconds: " + deleteMessageSeconds);
317    
318     if (duration) {
319     log("Added unban queue");
320    
321     await this.client.queueManager.add(
322     new QueueEntry({
323     args: [user.id],
324     client: this.client,
325     filePath: path.resolve(__dirname, "../queues/UnbanQueue"),
326     guild,
327     name: "UnbanQueue",
328     createdAt: new Date(),
329     userId: user.id,
330     willRunAt: new Date(Date.now() + duration)
331     })
332     );
333     }
334    
335     return id;
336     } catch (e) {
337     logError(e);
338     await this.autoRemoveUnbanQueue(guild, user).catch(logError);
339     return null;
340     }
341     }
342    
343     async createUserFakeBan(user: User, { guild, reason, notifyUser, duration }: CreateUserBanOptions) {
344     const id = Math.round(Math.random() * 1000);
345    
346     if (notifyUser) {
347     await this.sendDM(user, guild, {
348     id,
349     actionDoneName: "banned",
350     reason,
351     fields: duration
352     ? [
353     {
354     name: "Duration",
355     value: formatDistanceToNowStrict(new Date(Date.now() - duration))
356     }
357     ]
358     : undefined
359     });
360     }
361    
362     return id;
363     }
364    
365     async createUserShot(user: User, { guild, reason, moderator }: Omit<CommonOptions, "notifyUser" | "sendLog">) {
366     const id = Math.round(Math.random() * 1000);
367    
368     await this.sendDM(user, guild, {
369     title: `You have gotten a shot in ${guild.name}`,
370     id,
371     reason,
372     fields: [
373     {
374     name: `💉 Doctor`,
375     value: `${moderator.username}`
376     }
377     ],
378     color: 0x007bff
379     });
380    
381     return id;
382     }
383    
384     async createUserBean(user: User, { guild, moderator, reason }: Pick<CreateUserBanOptions, "guild" | "moderator" | "reason">) {
385     const { id } = await this.client.prisma.infraction.create({
386     data: {
387     type: InfractionType.BEAN,
388     userId: user.id,
389     guildId: guild.id,
390     reason,
391     moderatorId: moderator.id
392     }
393     });
394    
395     await this.sendDM(user, guild, {
396     id,
397     actionDoneName: "beaned",
398     reason,
399     color: 0x007bff
400     });
401    
402     return id;
403     }
404    
405     private async autoRemoveUnbanQueue(guild: Guild, user: User) {
406     log("Auto remove", this.client.queueManager.queues);
407    
408     for (const queue of this.client.queueManager.queues.values()) {
409     if (queue.options.name === "UnbanQueue" && queue.options.guild.id === guild.id && queue.options.args[0] === user.id) {
410     await this.client.queueManager.remove(queue);
411     }
412     }
413     }
414    
415     async removeUserBan(
416     user: User,
417     { guild, moderator, reason, autoRemoveQueue = true, sendLog }: CommonOptions & { autoRemoveQueue?: boolean }
418     ) {
419     if (autoRemoveQueue) await this.autoRemoveUnbanQueue(guild, user);
420    
421     try {
422     await guild.bans.remove(user);
423     } catch (error) {
424     logError(error);
425     return { error, noSuchBan: error instanceof DiscordAPIError && error.code === 10026 && error.status === 404 };
426     }
427    
428     const { id } = await this.client.prisma.infraction.create({
429     data: {
430     type: InfractionType.UNBAN,
431     userId: user.id,
432     guildId: guild.id,
433     reason,
434     moderatorId: moderator.id
435     }
436     });
437    
438     if (sendLog)
439     this.client.logger.logUserUnban({
440     moderator,
441     guild,
442     id: `${id}`,
443     user,
444     reason
445     });
446    
447     return { id };
448     }
449    
450     async createMemberKick(member: GuildMember, { guild, moderator, reason, notifyUser }: CommonOptions) {
451     if (!member.kickable) return null;
452    
453     const { id } = await this.client.prisma.infraction.create({
454     data: {
455     type: InfractionType.KICK,
456     userId: member.user.id,
457     guildId: guild.id,
458     reason,
459     moderatorId: moderator.id
460     }
461     });
462    
463     this.client.logger.logMemberKick({
464     moderator,
465     guild,
466     id: `${id}`,
467     member,
468     reason
469     });
470    
471     if (notifyUser) {
472     await this.sendDM(member.user, guild, {
473     id,
474     actionDoneName: "kicked",
475     reason
476     });
477     }
478    
479     try {
480     await member.kick(reason);
481     return id;
482     } catch (e) {
483     logError(e);
484     return null;
485     }
486     }
487    
488     async createMemberWarn(member: GuildMember, { guild, moderator, reason, notifyUser }: CommonOptions) {
489     const infraction = await this.client.prisma.infraction.create({
490     data: {
491     type: InfractionType.WARNING,
492     userId: member.user.id,
493     guildId: guild.id,
494     reason,
495     moderatorId: moderator.id
496     }
497     });
498     const { id } = infraction;
499    
500     this.client.logger.logMemberWarning({
501     moderator,
502     member,
503     guild,
504     id: `${id}`,
505     reason
506     });
507    
508     let result: boolean | null = false;
509    
510     if (notifyUser) {
511     result = await this.sendDM(member.user, guild, {
512     id,
513     actionDoneName: "warned",
514     reason,
515     fallback: true,
516     infraction
517     });
518     }
519    
520     return { id, result };
521     }
522    
523     bulkDeleteMessagesApplyFilters(message: Message, filters: Function[]) {
524     for (const filter of filters) {
525     if (!filter(message)) {
526     return false;
527     }
528     }
529    
530     return true;
531     }
532    
533     async bulkDeleteMessages({
534     user,
535     messagesToDelete,
536     messageChannel,
537     guild,
538     moderator,
539     reason,
540     sendLog,
541     count: messageCount,
542     logOnly = false,
543     filters = [],
544     offset = 0
545     }: BulkDeleteMessagesOptions) {
546     if (messageChannel && !(messagesToDelete && messagesToDelete.length === 0)) {
547     let messages: MessageResolvable[] | null = messagesToDelete ?? null;
548    
549     if (!logOnly && messages === null) {
550     log("The messagesToDelete was option not provided. Fetching messages manually.");
551    
552     try {
553     const allMessages = await messageChannel.messages.fetch({ limit: 100 });
554     messages = [];
555    
556     let i = 0;
557    
558     for (const [, m] of allMessages) {
559     if (i < offset) {
560     i++;
561     continue;
562     }
563    
564     if (messageCount && i >= messageCount) {
565     break;
566     }
567    
568     if (
569     (user ? m.author.id === user.id : true) &&
570     Date.now() - m.createdAt.getTime() <= 1000 * 60 * 60 * 24 * 7 * 2 &&
571     this.bulkDeleteMessagesApplyFilters(m, filters)
572     ) {
573     messages.push(m);
574     i++;
575     }
576     }
577     } catch (e) {
578     logError(e);
579     messages = null;
580     }
581     }
582    
583     const count = messages ? messages.length : 0;
584    
585     const { id } = user
586     ? await this.client.prisma.infraction.create({
587     data: {
588     type: InfractionType.BULK_DELETE_MESSAGE,
589     guildId: guild.id,
590     moderatorId: moderator.id,
591     userId: user.id,
592     metadata: {
593     count
594     },
595     reason
596     }
597     })
598     : { id: 0 };
599    
600     if (
601     sendLog &&
602     this.client.configManager.config[messageChannel.guildId!]?.logging?.enabled &&
603     this.client.configManager.config[messageChannel.guildId!]?.logging?.events.message_bulk_delete
604     ) {
605     this.client.logger
606     .logBulkDeleteMessages({
607     channel: messageChannel,
608     count,
609     guild,
610     id: id === 0 ? undefined : `${id}`,
611     user,
612     moderator,
613     reason,
614     messages: messages ?? []
615     })
616     .catch(logError);
617     }
618    
619     if (!logOnly && messages) {
620     try {
621     await messageChannel.bulkDelete(messages);
622     const reply = await messageChannel.send(
623     `${getEmoji(this.client, "check")} Deleted ${messages.length} messages${
624     user ? ` from user **@${escapeMarkdown(user.username)}**` : ""
625     }`
626     );
627    
628     setTimeout(() => reply.delete().catch(logError), 5000);
629     return true;
630     } catch (e) {
631     logError(e);
632     }
633     }
634    
635     return false;
636     }
637     }
638    
639     async createMemberMute(
640     member: GuildMember,
641     {
642     guild,
643     moderator,
644     reason,
645     notifyUser,
646     duration,
647     messagesToDelete,
648     messageChannel,
649     bulkDeleteReason,
650     sendLog,
651     autoRemoveQueue
652     }: CreateMemberMuteOptions
653     ) {
654     const mutedRole = this.client.configManager.config[guild.id]?.muting?.role;
655    
656     if (!mutedRole) {
657     if (!duration) {
658     return {
659     error: "Muted role is not configured and duration wasn't provided, please set the muted role to perform this operation."
660     };
661     }
662    
663     try {
664     await member.disableCommunicationUntil(new Date(Date.now() + duration), reason);
665     } catch (e) {
666     logError(e);
667     return { error: "Failed to timeout user, make sure I have enough permissions!" };
668     }
669     } else {
670     try {
671     await member.roles.add(mutedRole);
672     } catch (e) {
673     logError(e);
674     return {
675     error: "Failed to assign the muted role to this user. Make sure that I have enough permissions to do it."
676     };
677     }
678     }
679    
680     if (autoRemoveQueue) {
681     log("Auto remove", this.client.queueManager.queues);
682    
683     for (const queue of this.client.queueManager.queues.values()) {
684     if (
685     queue.options.name === "UnmuteQueue" &&
686     queue.options.guild.id === member.guild.id &&
687     queue.options.args[0] === member.user.id
688     ) {
689     log("Called");
690     await this.client.queueManager.remove(queue);
691     }
692     }
693     }
694    
695     let queueId: number | undefined;
696    
697     if (duration) {
698     queueId = await this.client.queueManager.add(
699     new QueueEntry({
700     args: [member.user.id],
701     guild,
702     client: this.client,
703     createdAt: new Date(),
704     filePath: path.resolve(__dirname, "../queues/UnmuteQueue"),
705     name: "UnmuteQueue",
706     userId: moderator.id,
707     willRunAt: new Date(Date.now() + duration)
708     })
709     );
710     }
711    
712     const infraction = await this.client.prisma.infraction.create({
713     data: {
714     type: InfractionType.MUTE,
715     userId: member.user.id,
716     guildId: guild.id,
717     reason,
718     moderatorId: moderator.id,
719     expiresAt: duration ? new Date(Date.now() + duration) : undefined,
720     queueId,
721     metadata: duration ? { duration } : undefined
722     }
723     });
724    
725     const { id } = infraction;
726    
727     if (sendLog) {
728     this.client.logger.logMemberMute({
729     moderator,
730     member,
731     guild,
732     id: `${id}`,
733     duration,
734     reason
735     });
736     }
737    
738     if (messageChannel && !(messagesToDelete && messagesToDelete.length === 0)) {
739     this.bulkDeleteMessages({
740     user: member.user,
741     guild,
742     moderator,
743     messagesToDelete,
744     messageChannel,
745     sendLog: true,
746     notifyUser: false,
747     reason:
748     bulkDeleteReason ??
749     `This user was muted with delete messages option specified. The mute reason was: ${
750     reason ?? "*No reason provided*"
751     }`
752     }).catch(logError);
753     }
754    
755     let result: boolean | null = !notifyUser;
756    
757     if (notifyUser) {
758     result = await this.sendDM(member.user, guild, {
759     id,
760     actionDoneName: "muted",
761     reason,
762     fields: [
763     {
764     name: "Duration",
765     value: `${duration ? formatDistanceToNowStrict(new Date(Date.now() - duration)) : "*No duration set*"}`
766     }
767     ],
768     fallback: true,
769     infraction
770     });
771     }
772    
773     return { id, result };
774     }
775    
776     async removeMemberMute(
777     member: GuildMember,
778     { guild, moderator, reason, notifyUser, sendLog, autoRemoveQueue = true }: CommonOptions & { autoRemoveQueue?: boolean }
779     ): Promise<{ error?: string; result?: boolean | null; id?: number }> {
780     const mutedRole = this.client.configManager.config[guild.id]?.muting?.role;
781    
782     if (!mutedRole) {
783     if (!member.isCommunicationDisabled()) {
784     return { error: "This user is not muted" };
785     }
786    
787     try {
788     await member.disableCommunicationUntil(null, reason);
789     } catch (e) {
790     logError(e);
791     return { error: "Failed to remove timeout from user, make sure I have enough permissions!" };
792     }
793     } else {
794     try {
795     await member.roles.remove(mutedRole);
796     } catch (e) {
797     logError(e);
798     return {
799     error: "Failed to remove the muted role to this user. Make sure that I have enough permissions to do it."
800     };
801     }
802     }
803    
804     const { id } = await this.client.prisma.infraction.create({
805     data: {
806     type: InfractionType.UNMUTE,
807     userId: member.user.id,
808     guildId: guild.id,
809     reason,
810     moderatorId: moderator.id
811     }
812     });
813    
814     if (sendLog) {
815     this.client.logger.logMemberUnmute({
816     moderator,
817     member,
818     guild,
819     id: `${id}`,
820     reason
821     });
822     }
823    
824     let result: boolean | null = !notifyUser;
825    
826     if (notifyUser) {
827     result = await this.sendDM(member.user, guild, {
828     id,
829     actionDoneName: "unmuted",
830     reason,
831     color: "Green"
832     });
833     }
834    
835     if (autoRemoveQueue) {
836     log("Auto remove", this.client.queueManager.queues);
837    
838     for (const queue of this.client.queueManager.queues.values()) {
839     if (
840     queue.options.name === "UnmuteQueue" &&
841     queue.options.guild.id === member.guild.id &&
842     queue.options.args[0] === member.user.id
843     ) {
844     log("Called");
845     await this.client.queueManager.remove(queue);
846     }
847     }
848     }
849    
850     return { id, result };
851     }
852    
853     async createUserMassBan({
854     users,
855     sendLog,
856     reason,
857     deleteMessageSeconds,
858     moderator,
859     guild,
860     callAfterEach,
861     callback
862     }: CreateUserMassBanOptions) {
863     if (users.length > 20) {
864     return { error: "Cannot perform this operation on more than 20 users" };
865     }
866    
867     const startTime = Date.now();
868    
869     const createInfractionData = [];
870     const skippedUsers: string[] = [];
871     const completedUsers: string[] = [];
872     let count = 0;
873     let calledJustNow = false;
874    
875     if (callback) {
876     await callback({
877     count,
878     users,
879     completedUsers,
880     skippedUsers
881     }).catch(logError);
882     }
883    
884     for (const user of users) {
885     if (callAfterEach && callback && count !== 0 && count % callAfterEach === 0) {
886     await callback({
887     count,
888     users,
889     completedUsers,
890     skippedUsers
891     }).catch(logError);
892    
893     calledJustNow = true;
894     } else calledJustNow = false;
895    
896     try {
897     await guild.bans.create(user, {
898     reason,
899     deleteMessageSeconds
900     });
901    
902     completedUsers.push(user);
903    
904     createInfractionData.push({
905     type: InfractionType.MASSBAN,
906     userId: user,
907     reason,
908     moderatorId: moderator.id,
909     guildId: guild.id,
910     metadata: {
911     deleteMessageSeconds
912     }
913     });
914     } catch (e) {
915     logError(e);
916     skippedUsers.push(user);
917     }
918    
919     count++;
920     }
921    
922     if (!calledJustNow && callback) {
923     await callback({
924     count,
925     users,
926     completedUsers,
927     skippedUsers,
928     completedIn: Math.round((Date.now() - startTime) / 1000)
929     }).catch(logError);
930     }
931    
932     await this.client.prisma.infraction.createMany({
933     data: createInfractionData
934     });
935    
936     if (sendLog)
937     await this.client.logger.logUserMassBan({
938     users: completedUsers,
939     reason,
940     guild,
941     moderator,
942     deleteMessageSeconds
943     });
944    
945     return { success: true };
946     }
947    
948     async createMemberMassKick({
949     users,
950     sendLog,
951     reason,
952     moderator,
953     guild,
954     callAfterEach,
955     callback
956     }: Omit<CreateUserMassBanOptions, "deleteMessageSeconds">) {
957     if (users.length > 10) {
958     return { error: "Cannot perform this operation on more than 10 users" };
959     }
960    
961     const startTime = Date.now();
962    
963     const createInfractionData = [];
964     const skippedUsers: string[] = [];
965     const completedUsers: string[] = [];
966     let count = 0;
967     let calledJustNow = false;
968    
969     if (callback) {
970     await callback({
971     count,
972     users,
973     completedUsers,
974     skippedUsers
975     }).catch(logError);
976     }
977    
978     for (const user of users) {
979     if (callAfterEach && callback && count !== 0 && count % callAfterEach === 0) {
980     await callback({
981     count,
982     users,
983     completedUsers,
984     skippedUsers
985     }).catch(logError);
986    
987     calledJustNow = true;
988     } else calledJustNow = false;
989    
990     try {
991     const member = guild.members.cache.get(user) ?? (await guild.members.fetch(user));
992     await member.kick(reason);
993    
994     completedUsers.push(user);
995    
996     createInfractionData.push({
997     type: InfractionType.MASSKICK,
998     userId: user,
999     reason,
1000     moderatorId: moderator.id,
1001     guildId: guild.id
1002     });
1003     } catch (e) {
1004     logError(e);
1005     skippedUsers.push(user);
1006     }
1007    
1008     count++;
1009     }
1010    
1011     if (!calledJustNow && callback) {
1012     await callback({
1013     count,
1014     users,
1015     completedUsers,
1016     skippedUsers,
1017     completedIn: Math.round((Date.now() - startTime) / 1000)
1018     }).catch(logError);
1019     }
1020    
1021     await this.client.prisma.infraction.createMany({
1022     data: createInfractionData
1023     });
1024    
1025     if (sendLog)
1026     await this.client.logger.logUserMassBan({
1027     users: completedUsers,
1028     reason,
1029     guild,
1030     moderator
1031     });
1032    
1033     return { success: true };
1034     }
1035    
1036     async createInfractionHistoryBuffer(user: User | APIUser, guild: Guild) {
1037     const infractions = await this.client.prisma.infraction.findMany({
1038     where: {
1039     userId: user.id,
1040     guildId: guild.id
1041     }
1042     });
1043    
1044     if (infractions.length === 0) {
1045     return { buffer: null, count: 0 };
1046     }
1047    
1048     let fileContents = `*** Infraction History ***\nUser: @${user.username} (${user.id})\nServer: ${guild.name} (${guild.id})\n`;
1049    
1050     fileContents += `Generated By: SudoBot/${this.client.metadata.data.version}\n`;
1051     fileContents += `Total Records: ${infractions.length}\n\n`;
1052    
1053     const table = new AsciiTable3("Infractions");
1054     const fields = ["Type", "Reason", "Date", "Duration"];
1055    
1056     if (this.client.configManager.config[guild.id]?.infractions?.send_ids_to_user) {
1057     fields.unshift("ID");
1058     }
1059    
1060     table.setHeading(...fields);
1061     table.setAlign(3, AlignmentEnum.CENTER);
1062     table.addRowMatrix(
1063     infractions.map(infraction => {
1064     const row: any[] = [
1065     infraction.type,
1066     infraction.reason ?? "*No reason provided*",
1067     `${infraction.createdAt.toUTCString()} (${formatDistanceToNowStrict(infraction.createdAt, {
1068     addSuffix: true
1069     })}) `,
1070     (infraction.metadata as any)?.duration
1071     ? formatDistanceToNowStrict(new Date(Date.now() - (infraction.metadata as any)?.duration))
1072     : "*None*"
1073     ];
1074    
1075     if (this.client.configManager.config[guild.id]?.infractions?.send_ids_to_user) {
1076     row.unshift(infraction.id);
1077     }
1078    
1079     return row;
1080     })
1081     );
1082    
1083     fileContents += table.toString();
1084     fileContents += "\n\n";
1085     fileContents += `Seeing something unexpected? Contact the staff of ${guild.name}.\n`;
1086    
1087     return { buffer: Buffer.from(fileContents), count: infractions.length };
1088     }
1089    
1090     private async notifyUserFallback({ infraction, user, guild, sendDMOptions, embed }: NotifyUserFallbackOptions) {
1091     const channelId = this.client.configManager.config[guild.id]?.infractions?.dm_fallback_parent_channel;
1092     const fallbackMode = this.client.configManager.config[guild.id]?.infractions?.dm_fallback ?? "none";
1093    
1094     if (!channelId || fallbackMode === "none") {
1095     return false;
1096     }
1097    
1098     const channel = await safeChannelFetch(guild, channelId);
1099    
1100     if (
1101     !channel ||
1102     (fallbackMode === "create_channel" && channel.type !== ChannelType.GuildCategory) ||
1103     (fallbackMode === "create_thread" && (!channel.isTextBased() || channel.isThread()))
1104     ) {
1105     return false;
1106     }
1107    
1108     if (fallbackMode === "create_channel") {
1109     return this.notifyUserInPrivateChannel({
1110     infraction,
1111     user,
1112     parentChannel: channel as TextChannel,
1113     sendDMOptions,
1114     embed
1115     });
1116     } else if (fallbackMode === "create_thread") {
1117     return this.notifyUserInPrivateThread({
1118     infraction,
1119     user,
1120     channel: channel as TextChannel,
1121     sendDMOptions,
1122     embed
1123     });
1124     }
1125    
1126     return true;
1127     }
1128    
1129     private async sendDMEmbedToChannel(
1130     channel: TextBasedChannel | PrivateThreadChannel,
1131     embed: EmbedBuilder,
1132     actionDoneName: ActionDoneName | undefined,
1133     user: User
1134     ) {
1135     const apiEmbed = embed.toJSON();
1136    
1137     const finalEmbed = {
1138     ...apiEmbed,
1139     author: {
1140     ...apiEmbed.author,
1141     name: `You have been ${actionDoneName ?? "given an infraction"}`
1142     }
1143     } satisfies APIEmbed;
1144    
1145     return await channel.send({
1146     content: user.toString(),
1147     embeds: [finalEmbed]
1148     });
1149     }
1150    
1151     private async notifyUserInPrivateChannel({
1152     infraction,
1153     parentChannel,
1154     user,
1155     sendDMOptions: { actionDoneName },
1156     embed
1157     }: Omit<NotifyUserFallbackOptions, "guild"> & { parentChannel: TextChannel }) {
1158     try {
1159     const expiresIn =
1160     this.client.configManager.config[parentChannel.guild.id]?.infractions?.dm_fallback_channel_expires_in;
1161    
1162     const channel = await parentChannel.guild.channels.create({
1163     name: `infraction-${infraction.id}`,
1164     type: ChannelType.GuildText,
1165     parent: parentChannel.id,
1166     reason: "Creating fallback channel to notify the user about their infraction",
1167     permissionOverwrites: [
1168     ...parentChannel.permissionOverwrites.cache.values(),
1169     {
1170     id: user.id,
1171     allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ReadMessageHistory]
1172     }
1173     ] satisfies OverwriteData[]
1174     });
1175    
1176     await this.sendDMEmbedToChannel(channel, embed, actionDoneName, user);
1177    
1178     if (expiresIn) {
1179     await this.client.queueManager.add(
1180     new QueueEntry({
1181     args: [channel.id],
1182     client: this.client,
1183     createdAt: new Date(),
1184     filePath: path.resolve(__dirname, "../queues/ChannelDeleteQueue"),
1185     guild: parentChannel.guild,
1186     name: "ChannelDeleteQueue",
1187     userId: this.client.user!.id,
1188     willRunAt: new Date(Date.now() + expiresIn)
1189     })
1190     );
1191     }
1192     } catch (e) {
1193     logError(e);
1194     return false;
1195     }
1196    
1197     return true;
1198     }
1199    
1200     private async notifyUserInPrivateThread({
1201     infraction,
1202     channel,
1203     user,
1204     sendDMOptions: { actionDoneName },
1205     embed
1206     }: Omit<NotifyUserFallbackOptions, "guild"> & { channel: TextChannel }) {
1207     try {
1208     const thread = (await channel.threads.create({
1209     name: `Infraction #${infraction.id}`,
1210     autoArchiveDuration: ThreadAutoArchiveDuration.OneWeek,
1211     type: ChannelType.PrivateThread,
1212     reason: "Creating fallback thread to notify the user about their infraction"
1213     })) as PrivateThreadChannel;
1214    
1215     await thread.members.add(user, "Adding the target user");
1216    
1217     await this.sendDMEmbedToChannel(thread, embed, actionDoneName, user);
1218     } catch (e) {
1219     logError(e);
1220     return false;
1221     }
1222    
1223     return true;
1224     }
1225     }
1226    
1227     interface NotifyUserFallbackOptions {
1228     infraction: Infraction;
1229     user: User;
1230     guild: Guild;
1231     sendDMOptions: SendDMOptions;
1232     embed: EmbedBuilder;
1233     }
1234    
1235     export type CreateUserMassBanOptions = Omit<
1236     CreateUserBanOptions & {
1237     users: readonly string[];
1238     callback?: (options: {
1239     count: number;
1240     users: readonly string[];
1241     completedUsers: readonly string[];
1242     skippedUsers: readonly string[];
1243     completedIn?: number;
1244     }) => Promise<any> | any;
1245     callAfterEach?: number;
1246     },
1247     "duration" | "autoRemoveQueue" | "notifyUser"
1248     >;
1249    
1250     export type CommonOptions = {
1251     reason?: string;
1252     guild: Guild;
1253     moderator: User;
1254     notifyUser?: boolean;
1255     sendLog?: boolean;
1256     };
1257    
1258     export type CreateUserBanOptions = CommonOptions & {
1259     deleteMessageSeconds?: number;
1260     duration?: number;
1261     autoRemoveQueue?: boolean;
1262     };
1263    
1264     export type CreateMemberMuteOptions = CommonOptions & {
1265     duration?: number;
1266     messagesToDelete?: MessageResolvable[];
1267     messageChannel?: TextChannel;
1268     bulkDeleteReason?: string;
1269     autoRemoveQueue?: boolean;
1270     };
1271    
1272     export type BulkDeleteMessagesOptions = CommonOptions & {
1273     user?: User;
1274     messagesToDelete?: MessageResolvable[];
1275     messageChannel?: TextChannel;
1276     count?: number;
1277     offset?: number;
1278     logOnly?: boolean;
1279     filters?: Function[];
1280     };
1281    
1282     export type ActionDoneName = "banned" | "muted" | "kicked" | "warned" | "unbanned" | "unmuted" | "softbanned" | "beaned";
1283    
1284     export type SendDMOptions = {
1285     fields?: APIEmbedField[] | ((internalFields: APIEmbedField[]) => Promise<APIEmbedField[]> | APIEmbedField[]);
1286     description?: string;
1287     actionDoneName?: ActionDoneName;
1288     id: string | number;
1289     reason?: string;
1290     color?: ColorResolvable;
1291     title?: string;
1292     fallback?: boolean;
1293     infraction?: Infraction;
1294     };

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26