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

Contents of /branches/6.x/src/services/InfractionManager.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: 40741 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 { 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