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

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26