/[sudobot]/branches/8.x/src/automod/MessageRuleService.ts
ViewVC logotype

Contents of /branches/8.x/src/automod/MessageRuleService.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: 32649 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 // TODO: Add an allow/disallow mode to all roles
21
22 import {
23 Attachment,
24 EmbedAssetData,
25 Message,
26 PermissionFlagsBits,
27 Snowflake,
28 TextChannel,
29 escapeCodeBlock,
30 escapeInlineCode,
31 escapeMarkdown
32 } from "discord.js";
33 import sharp from "sharp";
34 import Service from "../core/Service";
35 import { CreateLogEmbedOptions } from "../services/LoggerService";
36 import { HasEventListeners } from "../types/HasEventListeners";
37 import { MessageRuleType } from "../types/MessageRuleSchema";
38 import { log, logDebug, logError, logWarn } from "../utils/Logger";
39 import { escapeRegex, getEmoji, request } from "../utils/utils";
40
41 export const name = "messageRuleService";
42
43 type RuleHandlerMethod = Extract<keyof MessageRuleService, `rule${string}`>;
44
45 type RuleInfo =
46 | RuleHandlerMethod
47 | {
48 method: RuleHandlerMethod;
49 autoHandleModes?: boolean;
50 };
51
52 const handlers: Record<MessageRuleType["type"], RuleInfo> = {
53 domain: {
54 method: "ruleDomain",
55 autoHandleModes: false
56 },
57 blocked_file_extension: "ruleBlockedFileExtension",
58 blocked_mime_type: "ruleBlockedMimeType",
59 anti_invite: "ruleAntiInvite",
60 regex_filter: "ruleRegexFilter",
61 block_repeated_text: "ruleRepeatedText",
62 block_mass_mention: "ruleBlockMassMention",
63 regex_must_match: "ruleRegexMustMatch",
64 image: "ruleImage",
65 embed: "ruleEmbed",
66 EXPERIMENTAL_url_crawl: "ruleURLCrawl",
67 EXPERIMENTAL_nsfw_filter: "ruleNSFWFilter"
68 };
69
70 type MessageRuleAction = MessageRuleType["actions"][number];
71
72 // TODO: Allow specific words/tokens even after having rules that disallow it
73 // TODO: Introduce rule bypassers ^^
74
75 export default class MessageRuleService extends Service implements HasEventListeners {
76 private config(guildId: Snowflake) {
77 return this.client.configManager.config[guildId]?.message_rules;
78 }
79
80 async onMessageCreate(message: Message<boolean>) {
81 if (message.author.bot) {
82 return false;
83 }
84
85 const config = this.config(message.guildId!);
86
87 if (
88 !config?.enabled ||
89 config?.global_disabled_channels?.includes(message.channelId!) ||
90 (await this.client.permissionManager.isImmuneToAutoMod(message.member!, PermissionFlagsBits.ManageGuild))
91 ) {
92 return false;
93 }
94
95 return this.processMessageRules(message, config.rules);
96 }
97
98 private async processMessageRules(message: Message, rules: Array<MessageRuleType>) {
99 for (const rule of rules) {
100 if (rule.actions.length === 0) {
101 log("No action found in this rule! Considering it as disabled.");
102 continue;
103 }
104
105 if (rule.actions.length === 1 && rule.actions.includes("delete") && !message.deletable) {
106 log("Missing permissions to delete messages, but the rule actions include `delete`. Skipping.");
107 continue;
108 }
109
110 if (rule.actions.includes("mute") && rule.actions.includes("warn")) {
111 logWarn("You cannot include mute and warn together as message rule actions! Skipping.");
112 continue;
113 }
114
115 if (rule.disabled_channels.includes(message.channelId!)) {
116 logWarn("This rule is disabled in this channel.");
117 continue;
118 }
119
120 if (rule.immune_users.includes(message.author.id)) {
121 logWarn("This user is immune to this rule.");
122 continue;
123 }
124
125 if (message.member?.roles.cache.hasAny(...rule.immune_roles)) {
126 logWarn("This user is immune to this rule, due to having some whitelisted roles.");
127 continue;
128 }
129
130 const handlerFunctionInfo = handlers[rule.type];
131 const handlerFunctionName: RuleHandlerMethod | undefined =
132 typeof handlerFunctionInfo === "string" ? handlerFunctionInfo : handlerFunctionInfo?.method;
133 const handlerFunctionMetaInfo = (typeof handlerFunctionInfo === "string" ? null : handlerFunctionInfo) ?? {
134 method: handlerFunctionName,
135 autoHandleModes: true
136 };
137
138 if (!handlerFunctionInfo || !handlerFunctionName?.startsWith("rule")) {
139 continue;
140 }
141
142 const handler = this[handlerFunctionName] as (
143 ...args: unknown[]
144 ) => Promise<boolean | null | undefined | CreateLogEmbedOptions>;
145
146 if (typeof handler !== "function") {
147 continue;
148 }
149
150 try {
151 const result = await handler.call(this, message, rule);
152 const inverse = rule.mode === "inverse";
153 const { autoHandleModes } = handlerFunctionMetaInfo;
154
155 if ((result && !inverse) || (inverse && ((autoHandleModes && !result) || (!autoHandleModes && result)))) {
156 try {
157 for (const action of rule.actions) {
158 log("Taking action: ", action);
159 await this.takeAction(message, rule, action);
160 }
161
162 const embedOptions =
163 result && typeof result === "object" && !inverse
164 ? result
165 : inverse
166 ? {
167 options: {
168 description: `${getEmoji(this.client, "info")} This rule was __inversed__.`,
169 ...(result && typeof result === "object" && "options" in result ? result.options : {})
170 },
171 ...(result && typeof result === "object" ? result : {})
172 }
173 : undefined;
174
175 log(embedOptions);
176
177 await this.client.loggerService
178 .logMessageRuleAction({
179 message,
180 actions: rule.actions,
181 rule: rule.type,
182 embedOptions
183 })
184 .catch(logError);
185 } catch (e) {
186 logError(e);
187 }
188
189 return true;
190 }
191 } catch (e) {
192 logError(e);
193 continue;
194 }
195 }
196
197 return false;
198 }
199
200 private getReason(rule: MessageRuleType, key: Extract<keyof MessageRuleType, `${string}_reason`>) {
201 return rule[key] ?? rule.common_reason ?? "Your message violated the server rules. Please be careful next time.";
202 }
203
204 private async takeAction(message: Message, rule: MessageRuleType, action: MessageRuleAction) {
205 switch (action) {
206 case "delete":
207 if (message.deletable) {
208 await message.delete().catch(logError);
209 }
210
211 break;
212
213 case "verbal_warn":
214 {
215 const content = this.getReason(rule, "verbal_warning_reason");
216
217 await message.reply({
218 content
219 });
220 }
221
222 break;
223
224 case "warn":
225 {
226 const reason = this.getReason(rule, "warning_reason");
227
228 await this.client.infractionManager.createMemberWarn(message.member!, {
229 guild: message.guild!,
230 moderator: this.client.user!,
231 notifyUser: true,
232 reason,
233 sendLog: true
234 });
235 }
236
237 break;
238
239 case "mute":
240 {
241 const reason = this.getReason(rule, "mute_reason");
242
243 await this.client.infractionManager.createMemberMute(message.member!, {
244 guild: message.guild!,
245 moderator: this.client.user!,
246 notifyUser: true,
247 reason,
248 sendLog: true,
249 autoRemoveQueue: true,
250 duration: rule.mute_duration === -1 ? 60_000 : rule.mute_duration
251 });
252 }
253
254 break;
255
256 case "clear":
257 {
258 await this.client.infractionManager.bulkDeleteMessages({
259 guild: message.guild!,
260 moderator: this.client.user!,
261 count: 50,
262 sendLog: true,
263 messageChannel: message.channel! as TextChannel,
264 user: message.member!.user,
265 reason: "Message rule triggered"
266 });
267 }
268
269 break;
270 }
271 }
272
273 private scanForBlockedWordsAndTokens(tokens: string[] = [], words: string[] = [], ...strings: (string | null | undefined)[]) {
274 for (const string of strings) {
275 if (!string) {
276 continue;
277 }
278
279 for (const token of tokens) {
280 if (string.includes(token)) {
281 return { includes: true, token };
282 }
283 }
284
285 const splitted = string.split(/\s+/);
286
287 for (const word of words) {
288 if (splitted.includes(word)) {
289 return { includes: true, word };
290 }
291 }
292 }
293
294 return { includes: false };
295 }
296
297 async ruleNSFWFilter(message: Message, rule: Extract<MessageRuleType, { type: "EXPERIMENTAL_nsfw_filter" }>) {
298 logDebug("Scanning for NSFW content");
299
300 if (message.attachments.size === 0) {
301 return null;
302 }
303
304 const { score_thresholds } = rule;
305
306 for (const attachment of message.attachments.values()) {
307 logDebug("Scanning attachment", attachment.id);
308
309 if (attachment instanceof Attachment && attachment.contentType?.startsWith("image/")) {
310 logDebug("Scanning image attachment", attachment.id);
311
312 const [response, error] = await request({
313 url: attachment.proxyURL,
314 method: "GET",
315 responseType: "arraybuffer"
316 });
317
318 if (error || !response) {
319 logError(error);
320 return;
321 }
322
323 const imageData = Buffer.from(response.data, "binary");
324 const sharpMethodName = attachment.contentType.startsWith("image/gif")
325 ? "gif"
326 : attachment.contentType.startsWith("image/png")
327 ? "png"
328 : attachment.contentType.startsWith("image/jpeg")
329 ? "jpeg"
330 : "unknown";
331
332 if (sharpMethodName === "unknown") {
333 logWarn("Unknown image type");
334 continue;
335 }
336
337 const sharpInfo = sharp(imageData);
338 const sharpMethod = sharpInfo[sharpMethodName].bind(sharpInfo);
339 const convertedImageBuffer = await sharpMethod().toBuffer();
340 const result = await this.client.imageRecognitionService.detectNSFW(convertedImageBuffer);
341 const isNSFW =
342 result.hentai >= score_thresholds.hentai ||
343 result.porn >= score_thresholds.porn ||
344 result.sexy >= score_thresholds.sexy;
345
346 logDebug("NSFW result", result);
347
348 if (isNSFW) {
349 return {
350 title: "NSFW content detected in image",
351 fields: [
352 {
353 name: "Scores",
354 value: `Hentai: ${Math.round(result.hentai * 100)}%\nPorn: ${Math.round(
355 result.porn * 100
356 )}%\nSexy: ${Math.round(result.sexy * 100)}%\nNeutral: ${Math.round(result.neutral * 100)}%`
357 }
358 ]
359 } as CreateLogEmbedOptions;
360 }
361 }
362 }
363
364 return null;
365 }
366
367 async ruleEmbed(message: Message, rule: Extract<MessageRuleType, { type: "embed" }>) {
368 if (message.embeds.length === 0) {
369 return null;
370 }
371
372 const config = this.client.configManager.config[message.guildId!]?.message_filter;
373 const { mode, tokens, words, inherit_from_word_filter } = rule;
374
375 if (config?.enabled && inherit_from_word_filter) {
376 words.push(...config.data.blocked_words);
377 tokens.push(...config.data.blocked_tokens);
378 }
379
380 for (const embed of message.embeds) {
381 const fieldsStrings = embed.fields.reduce((acc, value) => {
382 acc.push(value.name, value.value);
383 return acc;
384 }, [] as string[]);
385 const { includes, word, token } = this.scanForBlockedWordsAndTokens(
386 tokens,
387 words,
388 embed.author?.name,
389 embed.description,
390 embed.title,
391 embed.footer?.text,
392 ...fieldsStrings
393 );
394
395 if (includes && mode === "normal") {
396 return {
397 title: `Blocked ${token ? "token" : word ? "word" : "word/token"}(s) detected in embed`,
398 fields: [
399 {
400 name: `${token ? "Token" : word ? "Word" : "Word/Token"}`,
401 value: `||${escapeMarkdown(token ?? word ?? "[???]")}||`
402 },
403 {
404 name: "Method",
405 value: "Embed Scan"
406 }
407 ]
408 } as CreateLogEmbedOptions;
409 } else if (!includes && mode === "inverse") {
410 return {
411 title: `Blocked ${token ? "token" : word ? "word" : "word/token"}(s) was not found in embed`,
412 fields: [
413 {
414 name: `${token ? "Token" : word ? "Word" : "Word/Token"}`,
415 value: `||${escapeMarkdown(token ?? word ?? "[???]")}||`
416 },
417 {
418 name: "Method",
419 value: "Embed Scan"
420 }
421 ]
422 } as CreateLogEmbedOptions;
423 }
424 }
425
426 return null;
427 }
428
429 async ruleImage(message: Message, rule: Extract<MessageRuleType, { type: "image" }>) {
430 if (message.attachments.size === 0 && (!rule.scan_embeds || message.embeds.length === 0)) {
431 return null;
432 }
433
434 const config = this.client.configManager.config[message.guildId!]?.message_filter;
435 const { mode, tokens, words, inherit_from_word_filter, scan_embeds } = rule;
436
437 if (config?.enabled && inherit_from_word_filter) {
438 words.push(...config.data.blocked_words);
439 tokens.push(...config.data.blocked_tokens);
440 }
441
442 const attachments: Array<EmbedAssetData | Attachment> = [...message.attachments.values()];
443
444 if (scan_embeds) {
445 for (const embed of message.embeds) {
446 if (embed.image) {
447 attachments.push(embed.image);
448 }
449
450 if (embed.thumbnail) {
451 attachments.push(embed.thumbnail);
452 }
453
454 if (embed.author?.proxyIconURL ?? embed.author?.iconURL) {
455 attachments.push({
456 url: embed.author?.proxyIconURL ?? embed.author?.iconURL ?? ""
457 });
458 }
459 }
460 }
461
462 for (const attachment of attachments) {
463 if (attachment instanceof Attachment && !attachment.contentType?.startsWith("image/")) {
464 log(`Not scanning attachment ${attachment.id} as it's not an image`);
465 continue;
466 }
467
468 const {
469 data: { text: actualText, words: textWords }
470 } = await this.client.imageRecognitionService.recognize(attachment.proxyURL ?? attachment.url);
471 const text = actualText.toLowerCase();
472
473 for (const token of tokens) {
474 const includes = text.includes(token.toLowerCase());
475
476 if (includes && mode === "normal") {
477 return {
478 title: "Blocked token(s) detected in image",
479 fields: [
480 {
481 name: "Token",
482 value: `||${escapeMarkdown(token)}||`
483 },
484 {
485 name: "Method",
486 value: "Image Scan"
487 }
488 ]
489 } satisfies CreateLogEmbedOptions;
490 } else if (!includes && mode === "inverse") {
491 return {
492 title: "Required token(s) were not found in image",
493 fields: [
494 {
495 name: "Token",
496 value: `||${escapeMarkdown(token)}||`
497 },
498 {
499 name: "Method",
500 value: "Image Scan"
501 }
502 ]
503 } satisfies CreateLogEmbedOptions;
504 }
505 }
506
507 for (const textWord of textWords) {
508 const includes = words.includes(textWord.text.toLowerCase());
509
510 if (includes && mode === "normal") {
511 return {
512 title: "Blocked word(s) detected in image",
513 fields: [
514 {
515 name: "Word",
516 value: `||${escapeMarkdown(textWord.text)}||`
517 },
518 {
519 name: "Method",
520 value: "Image Scan"
521 }
522 ]
523 } satisfies CreateLogEmbedOptions;
524 } else if (!includes && mode === "inverse") {
525 return {
526 title: "Required word(s) were not found in image",
527 fields: [
528 {
529 name: "Word",
530 value: `||${escapeMarkdown(textWord.text)}||`
531 },
532 {
533 name: "Method",
534 value: "Image Scan"
535 }
536 ]
537 } satisfies CreateLogEmbedOptions;
538 }
539 }
540 }
541
542 log("Image scan passed");
543 return null;
544 }
545
546 /** This rule is experimental. It needs caching support. */
547 async ruleURLCrawl(message: Message, rule: Extract<MessageRuleType, { type: "EXPERIMENTAL_url_crawl" }>) {
548 if (message.content.trim() === "") {
549 return null;
550 }
551
552 const { excluded_domains_regex, excluded_link_regex, excluded_links, words, tokens, inherit_from_word_filter } = rule;
553 const config = this.client.configManager.config[message.guildId!]?.message_filter;
554
555 const matches = message.content.matchAll(/https?:\/\/([A-Za-z0-9-.]*[A-Za-z0-9-])[\S]*/gim);
556
557 for (const match of matches) {
558 const url = match[0].toLowerCase();
559 const domain = match[1].toLowerCase();
560
561 if (excluded_links.includes(url)) {
562 return null;
563 }
564
565 for (const regex of excluded_domains_regex) {
566 if (new RegExp(regex, "gim").test(domain)) {
567 return null;
568 }
569 }
570
571 for (const regex of excluded_link_regex) {
572 if (new RegExp(regex, "gim").test(url)) {
573 return null;
574 }
575 }
576 }
577
578 if (config?.enabled && inherit_from_word_filter) {
579 words.push(...config.data.blocked_words);
580 tokens.push(...config.data.blocked_tokens);
581 }
582
583 for (const match of matches) {
584 const url = match[0].toLowerCase();
585
586 const [response, error] = await request({
587 url,
588 method: "GET",
589 transformResponse: r => r
590 });
591
592 if (error) {
593 logError(error);
594 continue;
595 }
596
597 if (typeof response?.data !== "string") {
598 logWarn("The response returned by the server during URL crawl is invalid");
599 continue;
600 }
601
602 const lowerCasedData = response.data.toLowerCase();
603
604 for (const token of tokens) {
605 if (lowerCasedData.includes(token)) {
606 return {
607 title: "Website contains blocked token(s)",
608 fields: [
609 {
610 name: "Token",
611 value: `||${token}||`
612 },
613 {
614 name: "Method",
615 value: "URL Crawling"
616 }
617 ]
618 } satisfies CreateLogEmbedOptions;
619 }
620 }
621
622 const textWords = lowerCasedData.split(/\s+/);
623
624 for (const word of words) {
625 if (textWords.includes(word)) {
626 return {
627 title: "Website contains blocked word(s)",
628 fields: [
629 {
630 name: "Word",
631 value: `||${word}||`
632 },
633 {
634 name: "Method",
635 value: "URL Crawling"
636 }
637 ]
638 } satisfies CreateLogEmbedOptions;
639 }
640 }
641 }
642
643 return null;
644 }
645
646 async ruleDomain(message: Message, rule: Extract<MessageRuleType, { type: "domain" }>) {
647 if (message.content.trim() === "") {
648 return null;
649 }
650
651 const { domains, scan_links_only, mode } = rule;
652
653 const prefix = scan_links_only ? "(https?://)" : "(https?://)?";
654 let specificRegex = `${prefix}(`;
655 const genericRegex = `${prefix}([a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})`;
656
657 let index = 0;
658 for (const domain of domains) {
659 const escapedDomain = escapeRegex(domain).replace(/\\\*/g, "[A-Za-z0-9-]+");
660 specificRegex += `${escapedDomain}`;
661
662 if (index < domains.length - 1) {
663 specificRegex += "|";
664 }
665
666 index++;
667 }
668
669 specificRegex += ")\\S*";
670 log(specificRegex);
671
672 const specificMatches = [...(new RegExp(specificRegex, "i").exec(message.content) ?? [])];
673 log(specificMatches);
674
675 const genericMatches = [...(new RegExp(genericRegex, "i").exec(message.content) ?? [])];
676 log(genericMatches);
677
678 if (specificMatches.length > 0) {
679 const cleanedDomain = (specificMatches[2] ?? specificMatches[1] ?? specificMatches[0]).replace(/https?:\/\//, "");
680 if (mode === "normal") {
681 return {
682 title: "Blocked domain(s) detected",
683 fields: [
684 {
685 name: "Domain",
686 value: `\`${escapeMarkdown(cleanedDomain)}\``
687 }
688 ]
689 } satisfies CreateLogEmbedOptions;
690 } else if (mode === "inverse") {
691 return false;
692 }
693 } else if (genericMatches.length > 0 && mode === "inverse" && !scan_links_only) {
694 const cleanedDomain = (genericMatches[2] ?? genericMatches[1] ?? genericMatches[0]).replace(/https?:\/\//, "");
695 return {
696 title: "Blocked domain(s) detected",
697 fields: [
698 {
699 name: "Domain",
700 value: `\`${escapeMarkdown(cleanedDomain)}\``
701 }
702 ]
703 } satisfies CreateLogEmbedOptions;
704 }
705
706 return false;
707 }
708
709 async ruleBlockedFileExtension(message: Message, rule: Extract<MessageRuleType, { type: "blocked_file_extension" }>) {
710 for (const attachment of message.attachments.values()) {
711 for (const extension of rule.data) {
712 if (attachment.proxyURL.endsWith(`.${extension}`)) {
713 return {
714 title: "File(s) with blocked extensions found",
715 fields: [
716 {
717 name: "File",
718 value: `[${attachment.name}](${attachment.url}): \`.${escapeMarkdown(extension)}\``
719 }
720 ]
721 };
722 }
723 }
724 }
725
726 return null;
727 }
728
729 async ruleBlockedMimeType(message: Message, rule: Extract<MessageRuleType, { type: "blocked_mime_type" }>) {
730 for (const attachment of message.attachments.values()) {
731 if (rule.data.includes(attachment.contentType ?? "unknown")) {
732 return {
733 title: "File(s) with blocked MIME-type found",
734 fields: [
735 {
736 name: "File",
737 value: `[${attachment.name}](${attachment.url}): \`${attachment.contentType}\``
738 }
739 ]
740 };
741 }
742 }
743
744 return null;
745 }
746
747 async ruleAntiInvite(message: Message, rule: Extract<MessageRuleType, { type: "anti_invite" }>) {
748 if (message.content.trim() === "") {
749 return null;
750 }
751
752 const allowedInviteCodes = rule.allowed_invite_codes;
753 const regex = /(https?:\/\/)?discord.(gg|com\/invite)\/([A-Za-z0-9_]+)/gi;
754 const matches = message.content.matchAll(regex);
755
756 for (const match of matches) {
757 if (match[3] && !allowedInviteCodes.includes(match[3])) {
758 if (rule.allow_internal_invites && this.client.inviteTracker.invites.has(`${message.guildId!}_${match[3]}`)) {
759 continue;
760 }
761
762 return {
763 title: "Posted Invite(s)",
764 fields: [
765 {
766 name: "Invite URL",
767 value: `\`https://discord.gg/${match[3]}\``
768 }
769 ]
770 };
771 }
772 }
773
774 return null;
775 }
776
777 async ruleRegexMustMatch(message: Message, rule: Extract<MessageRuleType, { type: "regex_must_match" }>) {
778 if (message.content.trim() === "") {
779 return null;
780 }
781
782 const { patterns } = rule;
783
784 for (const pattern of patterns) {
785 const regex = new RegExp(
786 typeof pattern === "string" ? pattern : pattern[0],
787 typeof pattern === "string" ? "gi" : pattern[1]
788 );
789
790 if (regex.test(message.content)) {
791 return null;
792 }
793 }
794
795 return {
796 title: "Message did not match with the specified regex patterns"
797 };
798 }
799
800 async ruleRegexFilter(message: Message, rule: Extract<MessageRuleType, { type: "regex_filter" }>) {
801 if (message.content.trim() === "") {
802 return null;
803 }
804
805 const { patterns } = rule;
806
807 for (const pattern of patterns) {
808 const regex = new RegExp(
809 typeof pattern === "string" ? pattern : pattern[0],
810 typeof pattern === "string" ? "gi" : pattern[1]
811 );
812
813 if (regex.test(message.content)) {
814 return {
815 title: "Message matched with a blocked regex pattern",
816 fields: [
817 {
818 name: "Pattern Info",
819 value: `Pattern: \`${escapeInlineCode(
820 escapeCodeBlock(typeof pattern === "string" ? pattern : pattern[0])
821 )}\`\nFlags: \`${escapeInlineCode(
822 escapeCodeBlock(typeof pattern === "string" ? "gi" : pattern[1])
823 )}\``
824 }
825 ]
826 };
827 }
828 }
829
830 return null;
831 }
832
833 async ruleRepeatedText(message: Message, rule: Extract<MessageRuleType, { type: "block_repeated_text" }>) {
834 if (message.content.trim() === "") {
835 return null;
836 }
837
838 if (new RegExp("(.+)\\1{" + rule.max_repeated_chars + ",}", "gm").test(message.content)) {
839 return {
840 title: "Repeated text detected",
841 fields: [
842 {
843 name: "Description",
844 value: "Too many repetitive characters were found"
845 }
846 ]
847 };
848 } else if (new RegExp("^(.+)(?: +\\1){" + rule.max_repeated_words + "}", "gm").test(message.content)) {
849 return {
850 title: "Repeated text detected",
851 fields: [
852 {
853 name: "Description",
854 value: "Too many repetitive words were found"
855 }
856 ]
857 };
858 }
859
860 return null;
861 }
862
863 async ruleBlockMassMention(message: Message, rule: Extract<MessageRuleType, { type: "block_mass_mention" }>) {
864 if (message.content.trim() === "") {
865 return null;
866 }
867
868 let data = [...message.content.matchAll(new RegExp("<@[0-9]+>", "gm"))];
869
870 console.log("users", data);
871
872 if (data.length >= rule.max_mentions || (rule.max_user_mentions > 0 && data.length >= rule.max_user_mentions)) {
873 return {
874 title: "Mass mentions detected",
875 fields: [
876 {
877 name: "Description",
878 value: "Too many users were mentioned"
879 }
880 ]
881 };
882 }
883
884 data = [...message.content.matchAll(new RegExp("<@&[0-9]+>", "gm"))];
885
886 if (data.length >= rule.max_mentions || (rule.max_role_mentions > 0 && data.length >= rule.max_role_mentions)) {
887 return {
888 title: "Repeated text detected",
889 fields: [
890 {
891 name: "Description",
892 value: "Too many roles were mentioned"
893 }
894 ]
895 };
896 }
897
898 return null;
899 }
900 }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26