aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIRONM00N <64110067+IRONM00N@users.noreply.github.com>2021-10-12 20:27:37 -0400
committerIRONM00N <64110067+IRONM00N@users.noreply.github.com>2021-10-12 20:27:37 -0400
commitba2d7b7db0a627234ed08de9d6bec8cb675404a7 (patch)
tree9ade9ed549b52eac3f2966a5cee5478267eca7c4
parentcac6abf3efd563b83f8f0ce70ce4bcfa5ada1a27 (diff)
downloadtanzanite-ba2d7b7db0a627234ed08de9d6bec8cb675404a7.tar.gz
tanzanite-ba2d7b7db0a627234ed08de9d6bec8cb675404a7.tar.bz2
tanzanite-ba2d7b7db0a627234ed08de9d6bec8cb675404a7.zip
revamp automod, refactoring, fixes
-rw-r--r--.gitignore2
-rw-r--r--lib/badwords.json556
-rw-r--r--package.json6
-rw-r--r--src/commands/info/botInfo.ts2
-rw-r--r--src/commands/moderation/ban.ts5
-rw-r--r--src/commands/moderation/kick.ts3
-rw-r--r--src/commands/moderation/modlog.ts2
-rw-r--r--src/commands/moderation/mute.ts3
-rw-r--r--src/commands/moderation/unmute.ts3
-rw-r--r--src/commands/moderation/warn.ts3
-rw-r--r--src/lib/badlinks.ts (renamed from lib/badlinks.json5)8
-rw-r--r--src/lib/badwords.ts242
-rw-r--r--src/lib/common/autoMod.ts236
-rw-r--r--src/lib/common/moderation.ts181
-rw-r--r--src/lib/extensions/discord-akairo/BushClientUtil.ts361
-rw-r--r--src/lib/extensions/discord.js/BushGuild.ts22
-rw-r--r--src/lib/extensions/discord.js/BushGuildMember.ts25
-rw-r--r--src/lib/models/Guild.ts25
-rw-r--r--src/listeners/client/interactionCreate.ts5
-rw-r--r--src/listeners/message/automodCreate.ts113
-rw-r--r--src/listeners/message/automodUpdate.ts4
-rw-r--r--tsconfig.json29
-rw-r--r--yarn.lock12
23 files changed, 860 insertions, 488 deletions
diff --git a/.gitignore b/.gitignore
index c72b1e4..207c3c0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,4 +36,4 @@ dist
# Options and credentials for the bot
src/config/options.ts
-lib/badlinks-secret.json5
+src/lib/badlinks-secret.ts
diff --git a/lib/badwords.json5 b/lib/badwords.json5
deleted file mode 100644
index eca13ca..0000000
--- a/lib/badwords.json5
+++ /dev/null
@@ -1,56 +0,0 @@
-/* Severities:
- - 0: Delete message
- - 1: Delete message and warn user
- - 2: Delete message and mute user for 15 minutes
- - 3: Delete message and mute user permanently
- */
-{
- /* Slurs */
- "faggot": 2,
- "nigga": 3,
- "nigger": 3,
- "nigra": 3,
- "retard": 2,
- "retarted": 2,
- "slut": 1,
- "tar baby": 2,
- "whore": 1,
- "卍": 3,
-
- /* Steam Scams */
- 'Я в тильте, в кс дали статус "Ненадежный"': 1, // I'm on tilt, in the cop they gave the status "Unreliable"
- "hello i am leaving cs:go": 3,
- "hello! I'm done with csgo": 3,
- "hi bro, i'm leaving this fucking game, take my skin": 3,
- "hi friend, today i am leaving this fucking game": 3,
- "hi guys, i'm leaving this fucking game, take my": 3,
- "hi, bro h am leaving cs:go and giving away my skin": 3,
- "hi, bro i am leaving cs:go and giving away my skin": 3,
- "i confirm all exchanges, there won't be enough": 3,
- "i quit csgo": 3,
- "the first three who send a trade": 3,
- "you can choose any skin for yourself": 3,
-
- /* Nitro Scams */
- "and there is discord hallween's giveaway": 3,
- "discord nitro for free - steam store": 3,
- "free 3 months of discord nitro": 3,
- "free discord nitro airdrop": 3,
- "get 3 months of discord nitro": 3,
- "get discord nitro for free": 3,
- "get free discord nitro from steam": 3,
- "lol, jahjajha free discord nitro for 3 month!!": 3,
- "steam is giving away 3 months of discord nitro for free to all no limited steam users": 3,
- "Лол, бесплатный дискорд нитро на 1 месяц!": 3, // Lol, 1 month free discord nitro!
- "Airdrop Discord FREE NITRO from Steam —": 3,
-
- /* Misc Scams */
- "found a cool software that improves the": 3,
-
- /* Frequently Advertised Discord Severs */
- "https://discord.gg/7CaCvDXs": 2,
-
- // 'tm5LcYN': 3,
- // '5gwLaOZ.png': 3,
- // 'hYKoQoU4bss': 3,
-}
diff --git a/package.json b/package.json
index 084df05..22fb91b 100644
--- a/package.json
+++ b/package.json
@@ -167,9 +167,9 @@
},
{
"files": [
- "badwords.json5",
- "badlinks.json5",
- "badlinks-secret.json5"
+ "badwords.ts",
+ "badlinks.ts",
+ "badlinks-secret.ts"
],
"options": {
"singleQuote": false,
diff --git a/src/commands/info/botInfo.ts b/src/commands/info/botInfo.ts
index 8f85fe6..257dc90 100644
--- a/src/commands/info/botInfo.ts
+++ b/src/commands/info/botInfo.ts
@@ -53,7 +53,7 @@ export default class BotInfoCommand extends BushCommand {
)
.addField('**CPU Usage**', `${client.stats.cpu}%`, true)
.addField('**Platform**', Platform[process.platform], true)
- .addField('**Commands Used**', `${client.stats.commandsUsed}`, true)
+ .addField('**Commands Used**', `${client.stats.commandsUsed.toLocaleString()}`, true)
.addField('**Servers**', client.guilds.cache.size.toLocaleString(), true)
.addField('**Users**', client.users.cache.size.toLocaleString(), true)
.addField('**Discord.js Version**', discordJSVersion, true)
diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts
index b64330f..4c2b3d3 100644
--- a/src/commands/moderation/ban.ts
+++ b/src/commands/moderation/ban.ts
@@ -1,5 +1,6 @@
import { AllowedMentions, BushCommand, BushMessage, BushSlashMessage } from '@lib';
import { Snowflake, User } from 'discord.js';
+import { Moderation } from '../../lib/common/moderation';
export default class BanCommand extends BushCommand {
public constructor() {
@@ -103,9 +104,7 @@ export default class BanCommand extends BushCommand {
const useForce = force && message.author.isOwner();
if (!message.member) throw new Error(`message.member is null`);
- const canModerateResponse = member
- ? await util.moderationPermissionCheck(message.member, member, 'ban', true, useForce)
- : true;
+ const canModerateResponse = member ? await Moderation.permissionCheck(message.member, member, 'ban', true, useForce) : true;
if (canModerateResponse !== true) {
return await message.util.reply(canModerateResponse);
diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts
index 07c25ab..715483a 100644
--- a/src/commands/moderation/kick.ts
+++ b/src/commands/moderation/kick.ts
@@ -1,4 +1,5 @@
import { AllowedMentions, BushCommand, BushGuildMember, BushMessage, BushSlashMessage, BushUser } from '@lib';
+import { Moderation } from '../../lib/common/moderation';
export default class KickCommand extends BushCommand {
public constructor() {
@@ -67,7 +68,7 @@ export default class KickCommand extends BushCommand {
);
if (!message.member) throw new Error(`message.member is null`);
const useForce = force && message.author.isOwner();
- const canModerateResponse = await util.moderationPermissionCheck(message.member, member, 'kick', true, useForce);
+ const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'kick', true, useForce);
if (canModerateResponse !== true) {
return message.util.reply(canModerateResponse);
diff --git a/src/commands/moderation/modlog.ts b/src/commands/moderation/modlog.ts
index 0eb7392..d5c6f91 100644
--- a/src/commands/moderation/modlog.ts
+++ b/src/commands/moderation/modlog.ts
@@ -48,7 +48,7 @@ export default class ModlogCommand extends BushCommand {
#generateModlogInfo(log: ModLog, showUser: boolean): string {
const trim = (str: string): string => (str.endsWith('\n') ? str.substring(0, str.length - 1).trim() : str.trim());
- const modLog = [`**Case ID**: ${log.id}`, `**Type**: ${log.type.toLowerCase()}`];
+ const modLog = [`**Case ID**: ${util.discord.escapeItalic(log.id)}`, `**Type**: ${log.type.toLowerCase()}`];
if (showUser) modLog.push(`**User**: <@!${log.user}>`);
modLog.push(`**Moderator**: <@!${log.moderator}>`);
if (log.duration) modLog.push(`**Duration**: ${util.humanizeDuration(log.duration)}`);
diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts
index 9e68d63..942c0b0 100644
--- a/src/commands/moderation/mute.ts
+++ b/src/commands/moderation/mute.ts
@@ -1,4 +1,5 @@
import { AllowedMentions, BushCommand, BushMessage, BushSlashMessage, BushUser } from '@lib';
+import { Moderation } from '../../lib/common/moderation';
export default class MuteCommand extends BushCommand {
public constructor() {
@@ -73,7 +74,7 @@ export default class MuteCommand extends BushCommand {
if (!message.member) throw new Error(`message.member is null`);
const useForce = force && message.author.isOwner();
- const canModerateResponse = await util.moderationPermissionCheck(message.member, member, 'mute', true, useForce);
+ const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'mute', true, useForce);
const victimBoldTag = `**${member.user.tag}**`;
if (canModerateResponse !== true) {
diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts
index 680c7ba..3d592b7 100644
--- a/src/commands/moderation/unmute.ts
+++ b/src/commands/moderation/unmute.ts
@@ -1,4 +1,5 @@
import { AllowedMentions, BushCommand, BushGuildMember, BushMessage, BushSlashMessage, BushUser } from '@lib';
+import { Moderation } from '../../lib/common/moderation';
export default class UnmuteCommand extends BushCommand {
public constructor() {
@@ -67,7 +68,7 @@ export default class UnmuteCommand extends BushCommand {
const useForce = force && message.author.isOwner();
- const canModerateResponse = await util.moderationPermissionCheck(message.member, member, 'unmute', true, useForce);
+ const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'unmute', true, useForce);
const victimBoldTag = `**${member.user.tag}**`;
diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts
index b4bf74d..3df4b3b 100644
--- a/src/commands/moderation/warn.ts
+++ b/src/commands/moderation/warn.ts
@@ -1,4 +1,5 @@
import { BushCommand, BushGuildMember, BushMessage, BushSlashMessage, BushUser } from '@lib';
+import { Moderation } from '../../lib/common/moderation';
export default class WarnCommand extends BushCommand {
public constructor() {
@@ -63,7 +64,7 @@ export default class WarnCommand extends BushCommand {
if (!member) return message.util.reply(`${util.emojis.error} I cannot warn users that are not in the server.`);
const useForce = force && message.author.isOwner();
if (!message.member) throw new Error(`message.member is null`);
- const canModerateResponse = await util.moderationPermissionCheck(message.member, member, 'warn', true, useForce);
+ const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'warn', true, useForce);
const victimBoldTag = `**${member.user.tag}**`;
if (canModerateResponse !== true) {
diff --git a/lib/badlinks.json5 b/src/lib/badlinks.ts
index 8fcfec0..67f9679 100644
--- a/lib/badlinks.json5
+++ b/src/lib/badlinks.ts
@@ -1,5 +1,7 @@
-/* Links in this file are treated as severity 3 offences. */
-[
+/* Links in this file are treated as severity 3 offences.
+
+made in part possible by https://github.com/nacrt/SkyblockClient-REPO/blob/main/files/scamlinks.json */
+export default [
"acercup.com",
"affix-cup.ru",
"affix-sport.ru",
@@ -405,4 +407,4 @@
"winskins.top",
"wintheskin.xyz",
"xgamercup.com",
-]
+];
diff --git a/src/lib/badwords.ts b/src/lib/badwords.ts
new file mode 100644
index 0000000..c5fbf2d
--- /dev/null
+++ b/src/lib/badwords.ts
@@ -0,0 +1,242 @@
+import { BadWords, Severity } from "./common/automod";
+
+export default {
+ /* -------------------------------------------------------------------------- */
+ /* Slurs */
+ /* -------------------------------------------------------------------------- */
+ "faggot": {
+ severity: Severity.TEMP_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "racial slur",
+ },
+ "nigga": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "racial slur",
+ },
+ "nigger": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "racial slur",
+ },
+ "nigra": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "racial slur",
+ },
+ "retard": {
+ severity: Severity.TEMP_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "ableist slur",
+ },
+ "retarted": {
+ severity: Severity.TEMP_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "ableist slur",
+ },
+ "slut": {
+ severity: Severity.WARN,
+ ignoreSpaces: false,
+ ignoreCapitalization: true,
+ reason: "derogatory term",
+ },
+ "tar baby": {
+ severity: Severity.TEMP_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "racial slur",
+ },
+ "whore": {
+ severity: Severity.WARN,
+ ignoreSpaces: false,
+ ignoreCapitalization: true,
+ reason: "derogatory term",
+ },
+ "卍": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "racist symbol",
+ },
+
+ /* -------------------------------------------------------------------------- */
+ /* Steam Scams */
+ /* -------------------------------------------------------------------------- */
+ 'Я в тильте, в кс дали статус "Ненадежный"': {
+ //? I'm on tilt, in the cop they gave the status "Unreliable"
+ severity: Severity.WARN,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "steam scam phrase",
+ },
+ "hello i am leaving cs:go": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "steam scam phrase",
+ },
+ "hello! I'm done with csgo": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "steam scam phrase",
+ },
+ "hi bro, i'm leaving this fucking game, take my skin": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "steam scam phrase",
+ },
+ "hi friend, today i am leaving this fucking game": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "steam scam phrase",
+ },
+ "hi guys, i'm leaving this fucking game, take my": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "steam scam phrase",
+ },
+ "hi, bro h am leaving cs:go and giving away my skin": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "steam scam phrase",
+ },
+ "hi, bro i am leaving cs:go and giving away my skin": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "steam scam phrase",
+ },
+ "i confirm all exchanges, there won't be enough": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "steam scam phrase",
+ },
+ "i quit csgo": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "steam scam phrase",
+ },
+ "the first three who send a trade": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "steam scam phrase",
+ },
+ "you can choose any skin for yourself": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "steam scam phrase",
+ },
+
+ /* -------------------------------------------------------------------------- */
+ /* Nitro Scams */
+ /* -------------------------------------------------------------------------- */
+ "and there is discord hallween's giveaway": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "discord nitro scam phrase",
+ },
+ "discord nitro for free - steam store": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "discord nitro scam phrase",
+ },
+ "free 3 months of discord nitro": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "discord nitro scam phrase",
+ },
+ "free discord nitro airdrop": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "discord nitro scam phrase",
+ },
+ "get 3 months of discord nitro": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "discord nitro scam phrase",
+ },
+ "get discord nitro for free": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "discord nitro scam phrase",
+ },
+ "get free discord nitro from steam": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "discord nitro scam phrase",
+ },
+ "lol, jahjajha free discord nitro for 3 month!!": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "discord nitro scam phrase",
+ },
+ "steam is giving away 3 months of discord nitro for free to all no limited steam users": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "discord nitro scam phrase",
+ },
+ "Лол, бесплатный дискорд нитро на 1 месяц!": {
+ //? Lol, 1 month free discord nitro!
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "discord nitro scam phrase",
+ },
+ "Airdrop Discord FREE NITRO from Steam —": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "discord nitro scam phrase",
+ },
+
+ /* -------------------------------------------------------------------------- */
+ /* Misc Scams */
+ /* -------------------------------------------------------------------------- */
+ "found a cool software that improves the": {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "misc. scam phrase",
+ },
+ "there is a possible chance tomorrow there will be a cyber-attack event where on all social networks including Discord there will be people trying":
+ {
+ severity: Severity.WARN,
+ ignoreSpaces: false,
+ ignoreCapitalization: true,
+ reason: "annoying copy pasta",
+ },
+
+ /* -------------------------------------------------------------------------- */
+ /* Frequently Advertised Discord Severs */
+ /* -------------------------------------------------------------------------- */
+ "https://discord.gg/7CaCvDXs": {
+ severity: Severity.TEMP_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: "blacklisted server link",
+ },
+} as BadWords;
diff --git a/src/lib/common/autoMod.ts b/src/lib/common/autoMod.ts
new file mode 100644
index 0000000..10bccba
--- /dev/null
+++ b/src/lib/common/autoMod.ts
@@ -0,0 +1,236 @@
+import { Formatters, MessageActionRow, MessageButton, MessageEmbed, TextChannel } from 'discord.js';
+import badLinksArray from '../../lib/badlinks';
+import badLinksSecretArray from '../../lib/badlinks-secret'; // I cannot make this public so just make a new file that export defaults an empty array
+import badWords from '../../lib/badwords';
+import { BushButtonInteraction } from '../extensions/discord.js/BushButtonInteraction';
+import { BushMessage } from '../extensions/discord.js/BushMessage';
+
+export class AutoMod {
+ private message: BushMessage;
+
+ public constructor(message: BushMessage) {
+ this.message = message;
+ void this.handle();
+ }
+
+ private async handle(): Promise<void> {
+ if (this.message.channel.type === 'DM' || !this.message.guild) return;
+ if (!(await this.message.guild.hasFeature('automod'))) return;
+
+ const customAutomodPhrases = (await this.message.guild.getSetting('autoModPhases')) ?? {};
+ const badLinks: BadWords = {};
+ const badLinksSecret: BadWords = {};
+
+ badLinksArray.forEach((link) => {
+ badLinks[link] = {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: 'malicious link'
+ };
+ });
+ badLinksSecretArray.forEach((link) => {
+ badLinks[link] = {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: 'malicious link'
+ };
+ });
+
+ const result = {
+ ...this.checkWords(customAutomodPhrases),
+ ...this.checkWords((await this.message.guild.hasFeature('excludeDefaultAutomod')) ? {} : badWords),
+ ...this.checkWords(
+ (await this.message.guild.hasFeature('excludeAutomodScamLinks')) ? {} : { ...badLinks, ...badLinksSecret }
+ )
+ };
+
+ if (Object.keys(result).length === 0) return;
+
+ const highestOffence = Object.entries(result)
+ .map(([key, value]) => ({ word: key, ...value }))
+ .sort((a, b) => b.severity - a.severity)[0];
+
+ if (highestOffence.severity === undefined || highestOffence.severity === null)
+ void this.message.guild.sendLogChannel('error', {
+ embeds: [
+ {
+ title: 'AutoMod Error',
+ description: `Unable to find severity information for ${Formatters.inlineCode(
+ util.discord.escapeInlineCode(highestOffence.word)
+ )}`,
+ color: util.colors.error
+ }
+ ]
+ });
+ else {
+ const color = this.punish(highestOffence);
+ void this.log(highestOffence, color, result);
+ }
+ }
+
+ private checkWords(words: BadWords): BadWords {
+ if (Object.keys(words).length === 0) return {};
+
+ const matchedWords: BadWords = {};
+ for (const word in words) {
+ const wordOptions = words[word];
+ if (this.format(this.message.content, wordOptions) === this.format(word, wordOptions)) {
+ }
+ }
+ return matchedWords;
+ }
+
+ private format(string: string, wordOptions: BadWordDetails) {
+ const temp = wordOptions.ignoreCapitalization ? string.toLowerCase() : string;
+ return wordOptions.ignoreSpaces ? temp.replace(/ /g, '') : temp;
+ }
+
+ private punish(highestOffence: BadWordDetails & { word: string }) {
+ let color;
+ switch (highestOffence.severity) {
+ case Severity.DELETE: {
+ color = util.colors.lightGray;
+ void this.message.delete().catch((e) => deleteError.bind(this, e));
+ break;
+ }
+ case Severity.WARN: {
+ color = util.colors.yellow;
+ void this.message.delete().catch((e) => deleteError.bind(this, e));
+ void this.message.member?.warn({
+ moderator: this.message.guild!.me!,
+ reason: `[AutoMod] ${highestOffence.reason}`
+ });
+ break;
+ }
+ case Severity.TEMP_MUTE: {
+ color = util.colors.orange;
+ void this.message.delete().catch((e) => deleteError.bind(this, e));
+ void this.message.member?.mute({
+ moderator: this.message.guild!.me!,
+ reason: `[AutoMod] ${highestOffence.reason}`,
+ duration: 900_000 // 15 minutes
+ });
+ break;
+ }
+ case Severity.PERM_MUTE: {
+ color = util.colors.red;
+ void this.message.delete().catch((e) => deleteError.bind(this, e));
+ void this.message.member?.mute({
+ moderator: this.message.guild!.me!,
+ reason: `[AutoMod] ${highestOffence.reason}`,
+ duration: 900_000 // 15 minutes
+ });
+ break;
+ }
+ default: {
+ throw new Error('Invalid severity');
+ }
+ }
+
+ return color;
+
+ async function deleteError(this: AutoMod, e: Error | any) {
+ this.message.guild?.sendLogChannel('error', {
+ embeds: [
+ {
+ title: 'AutoMod Error',
+ description: `Unable to delete triggered message.`,
+ fields: [{ name: 'Error', value: await util.codeblock(`${e.stack ?? e}`, 1024, 'js', true) }],
+ color: util.colors.error
+ }
+ ]
+ });
+ }
+ }
+
+ private async log(highestOffence: BadWordDetails & { word: string }, color: `#${string}`, offences: BadWords) {
+ void client.console.info(
+ 'autoMod',
+ `Severity <<${highestOffence.severity}>> action performed on <<${this.message.author.tag}>> (<<${
+ this.message.author.id
+ }>>) in <<#${(this.message.channel as TextChannel).name}>> in <<${this.message.guild!.name}>>`
+ );
+
+ return await this.message.guild!.sendLogChannel('automod', {
+ embeds: [
+ new MessageEmbed()
+ .setTitle(`[Severity ${highestOffence.severity}] Automod Action Performed`)
+ .setDescription(
+ `**User:** ${this.message.author} (${this.message.author.tag})\n**Sent From**: <#${
+ this.message.channel.id
+ }> [Jump to context](${this.message.url})\n**Blacklisted Words:** ${util
+ .surroundArray(Object.keys(offences), '`')
+ .join(', ')}`
+ )
+ .addField('Message Content', `${await util.codeblock(this.message.content, 1024)}`)
+ .setColor(color)
+ .setTimestamp()
+ ],
+ components:
+ highestOffence.severity >= 2
+ ? [
+ new MessageActionRow().addComponents(
+ new MessageButton()
+ .setStyle('DANGER')
+ .setLabel('Ban User')
+ .setCustomId(`automod;ban;${this.message.author.id};${highestOffence.reason}`)
+ )
+ ]
+ : undefined
+ });
+ }
+
+ public static async handleInteraction(interaction: BushButtonInteraction) {
+ if (!interaction.memberPermissions?.has('BAN_MEMBERS'))
+ return interaction.reply({
+ content: `${util.emojis.error} You are missing the **Ban Members** permission.`,
+ ephemeral: true
+ });
+ const [action, userId, reason] = interaction.customId.replace('automod;', '').split(';');
+ switch (action) {
+ case 'ban': {
+ await interaction.deferReply();
+ const result = await interaction.guild?.bushBan({ user: userId, reason, moderator: interaction.user.id });
+
+ if (result === 'success')
+ return interaction.reply({
+ content: `${util.emojis.success} Successfully banned **${
+ interaction.guild?.members.cache.get(userId)?.user.tag ?? userId
+ }**.`,
+ ephemeral: true
+ });
+ else
+ return interaction.reply({
+ content: `${util.emojis.error} Could not ban **${
+ interaction.guild?.members.cache.get(userId)?.user.tag ?? userId
+ }**: \`${result}\` .`,
+ ephemeral: true
+ });
+ }
+ }
+ }
+}
+
+export const enum Severity {
+ /** Delete message */
+ DELETE,
+ /** Delete message and warn user */
+ WARN,
+ /** Delete message and mute user for 15 minutes */
+ TEMP_MUTE,
+ /** Delete message and mute user permanently */
+ PERM_MUTE
+}
+
+interface BadWordDetails {
+ severity: Severity;
+ ignoreSpaces: boolean;
+ ignoreCapitalization: boolean;
+ reason: string;
+}
+
+export interface BadWords {
+ [key: string]: BadWordDetails;
+}
diff --git a/src/lib/common/moderation.ts b/src/lib/common/moderation.ts
new file mode 100644
index 0000000..4af6ec2
--- /dev/null
+++ b/src/lib/common/moderation.ts
@@ -0,0 +1,181 @@
+import { Snowflake } from 'discord-api-types';
+import {
+ ActivePunishment,
+ ActivePunishmentType,
+ BushGuildMember,
+ BushGuildMemberResolvable,
+ BushGuildResolvable,
+ Guild,
+ ModLog,
+ ModLogType
+} from '..';
+
+export class Moderation {
+ /**
+ * Checks if a moderator can perform a moderation action on another user.
+ * @param moderator - The person trying to perform the action.
+ * @param victim - The person getting punished.
+ * @param type - The type of punishment - used to format the response.
+ * @param checkModerator - Whether or not to check if the victim is a moderator.
+ */
+ public static async permissionCheck(
+ moderator: BushGuildMember,
+ victim: BushGuildMember,
+ type: 'mute' | 'unmute' | 'warn' | 'kick' | 'ban' | 'unban' | 'add a punishment role to' | 'remove a punishment role from',
+ checkModerator = true,
+ force = false
+ ): Promise<true | string> {
+ if (force) return true;
+
+ // If the victim is not in the guild anymore it will be undefined
+ if ((!victim || !victim.guild) && !['ban', 'unban'].includes(type)) return true;
+
+ if (moderator.guild.id !== victim.guild.id) {
+ throw new Error('moderator and victim not in same guild');
+ }
+
+ const isOwner = moderator.guild.ownerId === moderator.id;
+ if (moderator.id === victim.id && !type.startsWith('un')) {
+ return `${util.emojis.error} You cannot ${type} yourself.`;
+ }
+ if (
+ moderator.roles.highest.position <= victim.roles.highest.position &&
+ !isOwner &&
+ !(type.startsWith('un') && moderator.id === victim.id)
+ ) {
+ return `${util.emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as you do.`;
+ }
+ if (
+ victim.roles.highest.position >= victim.guild.me!.roles.highest.position &&
+ !(type.startsWith('un') && moderator.id === victim.id)
+ ) {
+ return `${util.emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as I do.`;
+ }
+ if (checkModerator && victim.permissions.has('MANAGE_MESSAGES') && !(type.startsWith('un') && moderator.id === victim.id)) {
+ if (await moderator.guild.hasFeature('modsCanPunishMods')) {
+ return true;
+ } else {
+ return `${util.emojis.error} You cannot ${type} **${victim.user.tag}** because they are a moderator.`;
+ }
+ }
+ return true;
+ }
+
+ public static async createModLogEntry(
+ options: {
+ type: ModLogType;
+ user: BushGuildMemberResolvable;
+ moderator: BushGuildMemberResolvable;
+ reason: string | undefined | null;
+ duration?: number;
+ guild: BushGuildResolvable;
+ pseudo?: boolean;
+ },
+ getCaseNumber = false
+ ): Promise<{ log: ModLog | null; caseNum: number | null }> {
+ const user = (await util.resolveNonCachedUser(options.user))!.id;
+ const moderator = (await util.resolveNonCachedUser(options.moderator))!.id;
+ const guild = client.guilds.resolveId(options.guild)!;
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ const duration = options.duration || undefined;
+
+ // If guild does not exist create it so the modlog can reference a guild.
+ await Guild.findOrCreate({
+ where: {
+ id: guild
+ },
+ defaults: {
+ id: guild
+ }
+ });
+
+ const modLogEntry = ModLog.build({
+ type: options.type,
+ user,
+ moderator,
+ reason: options.reason,
+ duration: duration,
+ guild,
+ pseudo: options.pseudo ?? false
+ });
+ const saveResult: ModLog | null = await modLogEntry.save().catch(async (e) => {
+ await util.handleError('createModLogEntry', e);
+ return null;
+ });
+
+ if (!getCaseNumber) return { log: saveResult, caseNum: null };
+
+ const caseNum = (await ModLog.findAll({ where: { type: options.type, user: user, guild: guild, hidden: 'false' } }))
+ ?.length;
+ return { log: saveResult, caseNum };
+ }
+
+ public static async createPunishmentEntry(options: {
+ type: 'mute' | 'ban' | 'role' | 'block';
+ user: BushGuildMemberResolvable;
+ duration: number | undefined;
+ guild: BushGuildResolvable;
+ modlog: string;
+ extraInfo?: Snowflake;
+ }): Promise<ActivePunishment | null> {
+ const expires = options.duration ? new Date(+new Date() + options.duration ?? 0) : undefined;
+ const user = (await util.resolveNonCachedUser(options.user))!.id;
+ const guild = client.guilds.resolveId(options.guild)!;
+ const type = this.#findTypeEnum(options.type)!;
+
+ const entry = ActivePunishment.build(
+ options.extraInfo
+ ? { user, type, guild, expires, modlog: options.modlog, extraInfo: options.extraInfo }
+ : { user, type, guild, expires, modlog: options.modlog }
+ );
+ return await entry.save().catch(async (e) => {
+ await util.handleError('createPunishmentEntry', e);
+ return null;
+ });
+ }
+
+ public static async removePunishmentEntry(options: {
+ type: 'mute' | 'ban' | 'role' | 'block';
+ user: BushGuildMemberResolvable;
+ guild: BushGuildResolvable;
+ extraInfo?: Snowflake;
+ }): Promise<boolean> {
+ const user = await util.resolveNonCachedUser(options.user);
+ const guild = client.guilds.resolveId(options.guild);
+ const type = this.#findTypeEnum(options.type);
+
+ if (!user || !guild) return false;
+
+ let success = true;
+
+ const entries = await ActivePunishment.findAll({
+ // finding all cases of a certain type incase there were duplicates or something
+ where: options.extraInfo
+ ? { user: user.id, guild: guild, type, extraInfo: options.extraInfo }
+ : { user: user.id, guild: guild, type }
+ }).catch(async (e) => {
+ await util.handleError('removePunishmentEntry', e);
+ success = false;
+ });
+ if (entries) {
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
+ entries.forEach(async (entry) => {
+ await entry.destroy().catch(async (e) => {
+ await util.handleError('removePunishmentEntry', e);
+ });
+ success = false;
+ });
+ }
+ return success;
+ }
+
+ static #findTypeEnum(type: 'mute' | 'ban' | 'role' | 'block') {
+ const typeMap = {
+ ['mute']: ActivePunishmentType.MUTE,
+ ['ban']: ActivePunishmentType.BAN,
+ ['role']: ActivePunishmentType.ROLE,
+ ['block']: ActivePunishmentType.BLOCK
+ };
+ return typeMap[type];
+ }
+}
diff --git a/src/lib/extensions/discord-akairo/BushClientUtil.ts b/src/lib/extensions/discord-akairo/BushClientUtil.ts
index a50cd61..448eaf3 100644
--- a/src/lib/extensions/discord-akairo/BushClientUtil.ts
+++ b/src/lib/extensions/discord-akairo/BushClientUtil.ts
@@ -3,16 +3,10 @@ import {
BushCache,
BushClient,
BushConstants,
- BushGuildMember,
- BushGuildMemberResolvable,
- BushGuildResolvable,
BushMessage,
BushSlashMessage,
BushUser,
Global,
- Guild,
- ModLog,
- ModLogType,
Pronoun,
PronounCode
} from '@lib';
@@ -54,7 +48,6 @@ import _ from 'lodash';
import moment from 'moment';
import { inspect, InspectOptions, promisify } from 'util';
import CommandErrorListener from '../../../listeners/commands/commandError';
-import { ActivePunishment, ActivePunishmentType } from '../../models/ActivePunishment';
import { BushNewsChannel } from '../discord.js/BushNewsChannel';
import { BushTextChannel } from '../discord.js/BushTextChannel';
import { BushSlashEditMessageType, BushSlashSendMessageType, BushUserResolvable } from './BushClient';
@@ -1079,174 +1072,6 @@ export class BushClientUtil extends ClientUtil {
return { duration, contentWithoutTime };
}
- /**
- * Checks if a moderator can perform a moderation action on another user.
- * @param moderator - The person trying to perform the action.
- * @param victim - The person getting punished.
- * @param type - The type of punishment - used to format the response.
- * @param checkModerator - Whether or not to check if the victim is a moderator.
- */
- public async moderationPermissionCheck(
- moderator: BushGuildMember,
- victim: BushGuildMember,
- type: 'mute' | 'unmute' | 'warn' | 'kick' | 'ban' | 'unban' | 'add a punishment role to' | 'remove a punishment role from',
- checkModerator = true,
- force = false
- ): Promise<true | string> {
- if (force) return true;
-
- // If the victim is not in the guild anymore it will be undefined
- if ((!victim || !victim.guild) && !['ban', 'unban'].includes(type)) return true;
-
- if (moderator.guild.id !== victim.guild.id) {
- throw new Error('moderator and victim not in same guild');
- }
-
- const isOwner = moderator.guild.ownerId === moderator.id;
- if (moderator.id === victim.id && !type.startsWith('un')) {
- return `${util.emojis.error} You cannot ${type} yourself.`;
- }
- if (
- moderator.roles.highest.position <= victim.roles.highest.position &&
- !isOwner &&
- !(type.startsWith('un') && moderator.id === victim.id)
- ) {
- return `${util.emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as you do.`;
- }
- if (
- victim.roles.highest.position >= victim.guild.me!.roles.highest.position &&
- !(type.startsWith('un') && moderator.id === victim.id)
- ) {
- return `${util.emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as I do.`;
- }
- if (checkModerator && victim.permissions.has('MANAGE_MESSAGES') && !(type.startsWith('un') && moderator.id === victim.id)) {
- if (await moderator.guild.hasFeature('modsCanPunishMods')) {
- return true;
- } else {
- return `${util.emojis.error} You cannot ${type} **${victim.user.tag}** because they are a moderator.`;
- }
- }
- return true;
- }
-
- public async createModLogEntry(
- options: {
- type: ModLogType;
- user: BushGuildMemberResolvable;
- moderator: BushGuildMemberResolvable;
- reason: string | undefined | null;
- duration?: number;
- guild: BushGuildResolvable;
- pseudo?: boolean;
- },
- getCaseNumber = false
- ): Promise<{ log: ModLog | null; caseNum: number | null }> {
- const user = (await util.resolveNonCachedUser(options.user))!.id;
- const moderator = (await util.resolveNonCachedUser(options.moderator))!.id;
- const guild = client.guilds.resolveId(options.guild)!;
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- const duration = options.duration || undefined;
-
- // If guild does not exist create it so the modlog can reference a guild.
- await Guild.findOrCreate({
- where: {
- id: guild
- },
- defaults: {
- id: guild
- }
- });
-
- const modLogEntry = ModLog.build({
- type: options.type,
- user,
- moderator,
- reason: options.reason,
- duration: duration,
- guild,
- pseudo: options.pseudo ?? false
- });
- const saveResult: ModLog | null = await modLogEntry.save().catch(async (e) => {
- await util.handleError('createModLogEntry', e);
- return null;
- });
-
- if (!getCaseNumber) return { log: saveResult, caseNum: null };
-
- const caseNum = (await ModLog.findAll({ where: { type: options.type, user: user, guild: guild, hidden: 'false' } }))
- ?.length;
- return { log: saveResult, caseNum };
- }
-
- public async createPunishmentEntry(options: {
- type: 'mute' | 'ban' | 'role' | 'block';
- user: BushGuildMemberResolvable;
- duration: number | undefined;
- guild: BushGuildResolvable;
- modlog: string;
- extraInfo?: Snowflake;
- }): Promise<ActivePunishment | null> {
- const expires = options.duration ? new Date(+new Date() + options.duration ?? 0) : undefined;
- const user = (await util.resolveNonCachedUser(options.user))!.id;
- const guild = client.guilds.resolveId(options.guild)!;
- const type = this.#findTypeEnum(options.type)!;
-
- const entry = ActivePunishment.build(
- options.extraInfo
- ? { user, type, guild, expires, modlog: options.modlog, extraInfo: options.extraInfo }
- : { user, type, guild, expires, modlog: options.modlog }
- );
- return await entry.save().catch(async (e) => {
- await util.handleError('createPunishmentEntry', e);
- return null;
- });
- }
-
- public async removePunishmentEntry(options: {
- type: 'mute' | 'ban' | 'role' | 'block';
- user: BushGuildMemberResolvable;
- guild: BushGuildResolvable;
- extraInfo?: Snowflake;
- }): Promise<boolean> {
- const user = await util.resolveNonCachedUser(options.user);
- const guild = client.guilds.resolveId(options.guild);
- const type = this.#findTypeEnum(options.type);
-
- if (!user || !guild) return false;
-
- let success = true;
-
- const entries = await ActivePunishment.findAll({
- // finding all cases of a certain type incase there were duplicates or something
- where: options.extraInfo
- ? { user: user.id, guild: guild, type, extraInfo: options.extraInfo }
- : { user: user.id, guild: guild, type }
- }).catch(async (e) => {
- await util.handleError('removePunishmentEntry', e);
- success = false;
- });
- if (entries) {
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
- entries.forEach(async (entry) => {
- await entry.destroy().catch(async (e) => {
- await util.handleError('removePunishmentEntry', e);
- });
- success = false;
- });
- }
- return success;
- }
-
- #findTypeEnum(type: 'mute' | 'ban' | 'role' | 'block') {
- const typeMap = {
- ['mute']: ActivePunishmentType.MUTE,
- ['ban']: ActivePunishmentType.BAN,
- ['role']: ActivePunishmentType.ROLE,
- ['block']: ActivePunishmentType.BLOCK
- };
- return typeMap[type];
- }
-
public humanizeDuration(duration: number, largest?: number): string {
if (largest) return humanizeDuration(duration, { language: 'en', maxDecimalPoints: 2, largest });
else return humanizeDuration(duration, { language: 'en', maxDecimalPoints: 2 });
@@ -1315,6 +1140,99 @@ export class BushClientUtil extends ClientUtil {
return string.charAt(0)?.toUpperCase() + string.slice(1);
}
+ /**
+ * Wait an amount in seconds.
+ */
+ public async sleep(s: number): Promise<unknown> {
+ return new Promise((resolve) => setTimeout(resolve, s * 1000));
+ }
+
+ public async handleError(context: string, error: Error) {
+ await client.console.error(_.camelCase(context), `An error occurred:\n${error?.stack ?? (error as any)}`, false);
+ await client.console.channelError({
+ embeds: [await CommandErrorListener.generateErrorEmbed({ type: 'unhandledRejection', error: error, context })]
+ });
+ }
+
+ public async resolveNonCachedUser(user: UserResolvable | undefined | null): Promise<BushUser | undefined> {
+ if (!user) return undefined;
+ const id =
+ user instanceof User || user instanceof GuildMember || user instanceof ThreadMember
+ ? user.id
+ : user instanceof Message
+ ? user.author.id
+ : typeof user === 'string'
+ ? user
+ : undefined;
+ if (!id) return undefined;
+ else return await client.users.fetch(id).catch(() => undefined);
+ }
+
+ public async getPronounsOf(user: User | Snowflake): Promise<Pronoun | undefined> {
+ const _user = await this.resolveNonCachedUser(user);
+ if (!_user) throw new Error(`Cannot find user ${user}`);
+ const apiRes = (await got
+ .get(`https://pronoundb.org/api/v1/lookup?platform=discord&id=${_user.id}`)
+ .json()
+ .catch(() => undefined)) as { pronouns: PronounCode } | undefined;
+
+ if (!apiRes) return undefined;
+ if (!apiRes.pronouns) throw new Error('apiRes.pronouns is undefined');
+
+ return client.constants.pronounMapping[apiRes.pronouns];
+ }
+
+ // modified from https://stackoverflow.com/questions/31054910/get-functions-methods-of-a-class
+ // answer by Bruno Grieder
+ public getMethods(_obj: any): string {
+ let props: string[] = [];
+ let obj: any = new Object(_obj);
+
+ do {
+ const l = Object.getOwnPropertyNames(obj)
+ .concat(Object.getOwnPropertySymbols(obj).map((s) => s.toString()))
+ .sort()
+ .filter(
+ (p, i, arr) =>
+ typeof Object.getOwnPropertyDescriptor(obj, p)?.['get'] !== 'function' && // ignore getters
+ typeof Object.getOwnPropertyDescriptor(obj, p)?.['set'] !== 'function' && // ignore setters
+ typeof obj[p] === 'function' && //only the methods
+ p !== 'constructor' && //not the constructor
+ (i == 0 || p !== arr[i - 1]) && //not overriding in this prototype
+ props.indexOf(p) === -1 //not overridden in a child
+ );
+
+ const reg = /\(([\s\S]*?)\)/;
+ props = props.concat(
+ l.map(
+ (p) =>
+ `${obj[p] && obj[p][Symbol.toStringTag] === 'AsyncFunction' ? 'async ' : ''}function ${p}(${
+ reg.exec(obj[p].toString())?.[1]
+ ? reg
+ .exec(obj[p].toString())?.[1]
+ .split(', ')
+ .map((arg) => arg.split('=')[0].trim())
+ .join(', ')
+ : ''
+ });`
+ )
+ );
+ } while (
+ (obj = Object.getPrototypeOf(obj)) && //walk-up the prototype chain
+ Object.getPrototypeOf(obj) //not the the Object prototype methods (hasOwnProperty, etc...)
+ );
+
+ return props.join('\n');
+ }
+
+ /**
+ * Removes all characters in a string that are either control characters or change the direction of text etc.
+ */
+ public sanitizeWtlAndControl(str: string) {
+ // eslint-disable-next-line no-control-regex
+ return str.replace(/[\u0000-\u001F\u007F-\u009F\u200B]/g, '');
+ }
+
get arg() {
return class Arg {
/**
@@ -1435,99 +1353,6 @@ export class BushClientUtil extends ClientUtil {
}
/**
- * Wait an amount in seconds.
- */
- public async sleep(s: number): Promise<unknown> {
- return new Promise((resolve) => setTimeout(resolve, s * 1000));
- }
-
- public async handleError(context: string, error: Error) {
- await client.console.error(_.camelCase(context), `An error occurred:\n${error?.stack ?? (error as any)}`, false);
- await client.console.channelError({
- embeds: [await CommandErrorListener.generateErrorEmbed({ type: 'unhandledRejection', error: error, context })]
- });
- }
-
- public async resolveNonCachedUser(user: UserResolvable | undefined | null): Promise<BushUser | undefined> {
- if (!user) return undefined;
- const id =
- user instanceof User || user instanceof GuildMember || user instanceof ThreadMember
- ? user.id
- : user instanceof Message
- ? user.author.id
- : typeof user === 'string'
- ? user
- : undefined;
- if (!id) return undefined;
- else return await client.users.fetch(id).catch(() => undefined);
- }
-
- public async getPronounsOf(user: User | Snowflake): Promise<Pronoun | undefined> {
- const _user = await this.resolveNonCachedUser(user);
- if (!_user) throw new Error(`Cannot find user ${user}`);
- const apiRes = (await got
- .get(`https://pronoundb.org/api/v1/lookup?platform=discord&id=${_user.id}`)
- .json()
- .catch(() => undefined)) as { pronouns: PronounCode } | undefined;
-
- if (!apiRes) return undefined;
- if (!apiRes.pronouns) throw new Error('apiRes.pronouns is undefined');
-
- return client.constants.pronounMapping[apiRes.pronouns];
- }
-
- // modified from https://stackoverflow.com/questions/31054910/get-functions-methods-of-a-class
- // answer by Bruno Grieder
- public getMethods(_obj: any): string {
- let props: string[] = [];
- let obj: any = new Object(_obj);
-
- do {
- const l = Object.getOwnPropertyNames(obj)
- .concat(Object.getOwnPropertySymbols(obj).map((s) => s.toString()))
- .sort()
- .filter(
- (p, i, arr) =>
- typeof Object.getOwnPropertyDescriptor(obj, p)?.['get'] !== 'function' && // ignore getters
- typeof Object.getOwnPropertyDescriptor(obj, p)?.['set'] !== 'function' && // ignore setters
- typeof obj[p] === 'function' && //only the methods
- p !== 'constructor' && //not the constructor
- (i == 0 || p !== arr[i - 1]) && //not overriding in this prototype
- props.indexOf(p) === -1 //not overridden in a child
- );
-
- const reg = /\(([\s\S]*?)\)/;
- props = props.concat(
- l.map(
- (p) =>
- `${obj[p] && obj[p][Symbol.toStringTag] === 'AsyncFunction' ? 'async ' : ''}function ${p}(${
- reg.exec(obj[p].toString())?.[1]
- ? reg
- .exec(obj[p].toString())?.[1]
- .split(', ')
- .map((arg) => arg.split('=')[0].trim())
- .join(', ')
- : ''
- });`
- )
- );
- } while (
- (obj = Object.getPrototypeOf(obj)) && //walk-up the prototype chain
- Object.getPrototypeOf(obj) //not the the Object prototype methods (hasOwnProperty, etc...)
- );
-
- return props.join('\n');
- }
-
- /**
- * Removes all characters in a string that are either control characters or change the direction of text etc.
- */
- public sanitizeWtlAndControl(str: string) {
- // eslint-disable-next-line no-control-regex
- return str.replace(/[\u0000-\u001F\u007F-\u009F\u200B]/g, '');
- }
-
- /**
* Discord.js's Util class
*/
get discord() {
diff --git a/src/lib/extensions/discord.js/BushGuild.ts b/src/lib/extensions/discord.js/BushGuild.ts
index 256b9dc..3a2ae51 100644
--- a/src/lib/extensions/discord.js/BushGuild.ts
+++ b/src/lib/extensions/discord.js/BushGuild.ts
@@ -1,5 +1,6 @@
-import { Guild, UserResolvable } from 'discord.js';
+import { Guild, MessageOptions, UserResolvable } from 'discord.js';
import { RawGuildData } from 'discord.js/typings/rawDataTypes';
+import { Moderation } from '../../common/moderation';
import { Guild as GuildDB, GuildFeatures, GuildLogType, GuildModel } from '../../models/Guild';
import { ModLogType } from '../../models/ModLog';
import { BushClient, BushUserResolvable } from '../discord-akairo/BushClient';
@@ -96,7 +97,7 @@ export class BushGuild extends Guild {
if (!banSuccess) return 'error banning';
// add modlog entry
- const { log: modlog } = await util.createModLogEntry({
+ const { log: modlog } = await Moderation.createModLogEntry({
type: options.duration ? ModLogType.TEMP_BAN : ModLogType.PERM_BAN,
user: user,
moderator: moderator.id,
@@ -108,7 +109,7 @@ export class BushGuild extends Guild {
caseID = modlog.id;
// add punishment entry so they can be unbanned later
- const punishmentEntrySuccess = await util.createPunishmentEntry({
+ const punishmentEntrySuccess = await Moderation.createPunishmentEntry({
type: 'ban',
user: user,
guild: this,
@@ -161,7 +162,7 @@ export class BushGuild extends Guild {
if (!unbanSuccess) return 'error unbanning';
// add modlog entry
- const { log: modlog } = await util.createModLogEntry({
+ const { log: modlog } = await Moderation.createModLogEntry({
type: ModLogType.UNBAN,
user: user.id,
moderator: moderator.id,
@@ -172,7 +173,7 @@ export class BushGuild extends Guild {
caseID = modlog.id;
// remove punishment entry
- const removePunishmentEntrySuccess = await util.removePunishmentEntry({
+ const removePunishmentEntrySuccess = await Moderation.removePunishmentEntry({
type: 'ban',
user: user.id,
guild: this
@@ -192,4 +193,15 @@ export class BushGuild extends Guild {
client.emit('bushUnban', user, moderator, this, options.reason ?? undefined, caseID!, dmSuccessEvent!);
return ret;
}
+
+ /**
+ * Sends a message to the guild's specified logging channel.
+ */
+ public async sendLogChannel(logType: GuildLogType, message: MessageOptions) {
+ const logChannel = await this.getLogChannel(logType);
+ if (!logChannel || logChannel.type !== 'GUILD_TEXT') return;
+ if (!logChannel.permissionsFor(this.me!.id)?.has(['VIEW_CHANNEL', 'SEND_MESSAGES', 'EMBED_LINKS'])) return;
+
+ return await logChannel.send(message).catch(() => null);
+ }
}
diff --git a/src/lib/extensions/discord.js/BushGuildMember.ts b/src/lib/extensions/discord.js/BushGuildMember.ts
index 8e855f7..b4c136c 100644
--- a/src/lib/extensions/discord.js/BushGuildMember.ts
+++ b/src/lib/extensions/discord.js/BushGuildMember.ts
@@ -1,5 +1,6 @@
import { GuildMember, MessageEmbed, Partialize, Role } from 'discord.js';
import { RawGuildMemberData } from 'discord.js/typings/rawDataTypes';
+import { Moderation } from '../../common/moderation';
import { ModLogType } from '../../models/ModLog';
import { BushClient } from '../discord-akairo/BushClient';
import { BushGuild } from './BushGuild';
@@ -111,7 +112,7 @@ export class BushGuildMember extends GuildMember {
const ret = await (async () => {
// add modlog entry
- const result = await util.createModLogEntry(
+ const result = await Moderation.createModLogEntry(
{
type: ModLogType.WARN,
user: this,
@@ -145,7 +146,7 @@ export class BushGuildMember extends GuildMember {
const ret = await (async () => {
if (options.addToModlog || options.duration) {
- const { log: modlog } = await util.createModLogEntry({
+ const { log: modlog } = await Moderation.createModLogEntry({
type: options.duration ? ModLogType.TEMP_PUNISHMENT_ROLE : ModLogType.PERM_PUNISHMENT_ROLE,
guild: this.guild,
moderator: moderator.id,
@@ -158,7 +159,7 @@ export class BushGuildMember extends GuildMember {
caseID = modlog.id;
if (options.addToModlog || options.duration) {
- const punishmentEntrySuccess = await util.createPunishmentEntry({
+ const punishmentEntrySuccess = await Moderation.createPunishmentEntry({
type: 'role',
user: this,
guild: this.guild,
@@ -198,7 +199,7 @@ export class BushGuildMember extends GuildMember {
const ret = await (async () => {
if (options.addToModlog) {
- const { log: modlog } = await util.createModLogEntry({
+ const { log: modlog } = await Moderation.createModLogEntry({
type: ModLogType.REMOVE_PUNISHMENT_ROLE,
guild: this.guild,
moderator: moderator.id,
@@ -209,7 +210,7 @@ export class BushGuildMember extends GuildMember {
if (!modlog) return 'error creating modlog entry';
caseID = modlog.id;
- const punishmentEntrySuccess = await util.removePunishmentEntry({
+ const punishmentEntrySuccess = await Moderation.removePunishmentEntry({
type: 'role',
user: this,
guild: this.guild,
@@ -281,7 +282,7 @@ export class BushGuildMember extends GuildMember {
if (!muteSuccess) return 'error giving mute role';
// add modlog entry
- const { log: modlog } = await util.createModLogEntry({
+ const { log: modlog } = await Moderation.createModLogEntry({
type: options.duration ? ModLogType.TEMP_MUTE : ModLogType.PERM_MUTE,
user: this,
moderator: moderator.id,
@@ -294,7 +295,7 @@ export class BushGuildMember extends GuildMember {
caseID = modlog.id;
// add punishment entry so they can be unmuted later
- const punishmentEntrySuccess = await util.createPunishmentEntry({
+ const punishmentEntrySuccess = await Moderation.createPunishmentEntry({
type: 'mute',
user: this,
guild: this.guild,
@@ -351,7 +352,7 @@ export class BushGuildMember extends GuildMember {
if (!muteSuccess) return 'error removing mute role';
//remove modlog entry
- const { log: modlog } = await util.createModLogEntry({
+ const { log: modlog } = await Moderation.createModLogEntry({
type: ModLogType.UNMUTE,
user: this,
moderator: moderator.id,
@@ -363,7 +364,7 @@ export class BushGuildMember extends GuildMember {
caseID = modlog.id;
// remove mute entry
- const removePunishmentEntrySuccess = await util.removePunishmentEntry({
+ const removePunishmentEntrySuccess = await Moderation.removePunishmentEntry({
type: 'mute',
user: this,
guild: this.guild
@@ -402,7 +403,7 @@ export class BushGuildMember extends GuildMember {
if (!kickSuccess) return 'error kicking';
// add modlog entry
- const { log: modlog } = await util.createModLogEntry({
+ const { log: modlog } = await Moderation.createModLogEntry({
type: ModLogType.KICK,
user: this,
moderator: moderator.id,
@@ -439,7 +440,7 @@ export class BushGuildMember extends GuildMember {
if (!banSuccess) return 'error banning';
// add modlog entry
- const { log: modlog } = await util.createModLogEntry({
+ const { log: modlog } = await Moderation.createModLogEntry({
type: options.duration ? ModLogType.TEMP_BAN : ModLogType.PERM_BAN,
user: this,
moderator: moderator.id,
@@ -451,7 +452,7 @@ export class BushGuildMember extends GuildMember {
caseID = modlog.id;
// add punishment entry so they can be unbanned later
- const punishmentEntrySuccess = await util.createPunishmentEntry({
+ const punishmentEntrySuccess = await Moderation.createPunishmentEntry({
type: 'ban',
user: this,
guild: this.guild,
diff --git a/src/lib/models/Guild.ts b/src/lib/models/Guild.ts
index 997be6a..f1ea43b 100644
--- a/src/lib/models/Guild.ts
+++ b/src/lib/models/Guild.ts
@@ -1,5 +1,6 @@
import { Snowflake } from 'discord.js';
import { DataTypes, Sequelize } from 'sequelize';
+import { BadWords } from '../common/automod';
import { BushClient } from '../extensions/discord-akairo/BushClient';
import { BaseModel } from './BaseModel';
import { jsonArrayInit, jsonParseGet, jsonParseSet, NEVER_USED } from './__helpers';
@@ -109,6 +110,14 @@ export const guildFeaturesObj = asGuildFeature({
name: 'Automod',
description: 'Deletes offensive content as well as phishing links.'
},
+ excludeDefaultAutomod: {
+ name: 'Exclude Default Automod',
+ description: 'Opt out of using the default automod options.'
+ },
+ excludeAutomodScamLinks: {
+ name: 'Exclude Automod Scam Links',
+ description: 'Opt out of having automod delete scam links.'
+ },
autoPublish: {
name: 'Auto Publish',
description: 'Publishes messages in configured announcement channels.'
@@ -159,6 +168,10 @@ export const guildLogsObj = {
report: {
description: 'Logs user reports.',
configurable: true
+ },
+ error: {
+ description: 'Logs errors that occur with the bot.',
+ configurable: true
}
};
export type GuildLogType = keyof typeof guildLogsObj;
@@ -181,7 +194,7 @@ export interface GuildModel {
punishmentEnding: string;
disabledCommands: string[];
lockdownChannels: Snowflake[];
- autoModPhases: { [word: string]: 0 | 1 | 2 | 3 };
+ autoModPhases: BadWords;
enabledFeatures: GuildFeatures[];
joinRoles: Snowflake[];
logChannels: LogChannelDB;
@@ -202,7 +215,7 @@ export interface GuildModelCreationAttributes {
punishmentEnding?: string;
disabledCommands?: string[];
lockdownChannels?: Snowflake[];
- autoModPhases?: { [word: string]: 0 | 1 | 2 | 3 };
+ autoModPhases?: BadWords;
enabledFeatures?: GuildFeatures[];
joinRoles?: Snowflake[];
logChannels?: LogChannelDB;
@@ -316,10 +329,10 @@ export class Guild extends BaseModel<GuildModel, GuildModelCreationAttributes> i
/**
* Custom automod phases
*/
- public get autoModPhases(): { [word: string]: 0 | 1 | 2 | 3 } {
+ public get autoModPhases(): BadWords {
throw new Error(NEVER_USED);
}
- public set autoModPhases(_: { [word: string]: 0 | 1 | 2 | 3 }) {
+ public set autoModPhases(_: BadWords) {
throw new Error(NEVER_USED);
}
@@ -424,10 +437,10 @@ export class Guild extends BaseModel<GuildModel, GuildModelCreationAttributes> i
lockdownChannels: jsonArrayInit('lockdownChannels'),
autoModPhases: {
type: DataTypes.TEXT,
- get: function (): { [level: number]: Snowflake } {
+ get: function (): BadWords {
return jsonParseGet.call(this, 'autoModPhases');
},
- set: function (val: { [level: number]: Snowflake }) {
+ set: function (val: BadWords) {
return jsonParseSet.call(this, 'autoModPhases', val);
},
allowNull: false,
diff --git a/src/listeners/client/interactionCreate.ts b/src/listeners/client/interactionCreate.ts
index d07a084..52ad2ec 100644
--- a/src/listeners/client/interactionCreate.ts
+++ b/src/listeners/client/interactionCreate.ts
@@ -1,4 +1,5 @@
-import { BushListener } from '@lib';
+import { BushButtonInteraction, BushListener } from '@lib';
+import { AutoMod } from '../../lib/common/automod';
import { BushClientEvents } from '../../lib/extensions/discord.js/BushClientEvents';
export default class InteractionCreateListener extends BushListener {
@@ -22,6 +23,8 @@ export default class InteractionCreateListener extends BushListener {
return;
} else if (interaction.isButton()) {
if (interaction.customId.startsWith('paginate_') || interaction.customId.startsWith('command_')) return;
+ else if (interaction.customId.startsWith('automod;'))
+ void AutoMod.handleInteraction(interaction as BushButtonInteraction);
return await interaction.reply({ content: 'Buttons go brrr', ephemeral: true });
} else if (interaction.isSelectMenu()) {
if (interaction.customId.startsWith('command_')) return;
diff --git a/src/listeners/message/automodCreate.ts b/src/listeners/message/automodCreate.ts
index 01fc803..3e98117 100644
--- a/src/listeners/message/automodCreate.ts
+++ b/src/listeners/message/automodCreate.ts
@@ -1,11 +1,5 @@
-import { BushListener, BushMessage } from '@lib';
-// @ts-expect-error: ts doesn't recognize json5
-import _badLinks from '@root/lib/badlinks'; // partially uses https://github.com/nacrt/SkyblockClient-REPO/blob/main/files/scamlinks.json
-// @ts-expect-error: ts doesn't recognize json5
-import _badLinksSecret from '@root/lib/badlinks-secret'; // shhhh
-// @ts-expect-error: ts doesn't recognize json5
-import _badWords from '@root/lib/badwords';
-import { MessageEmbed } from 'discord.js';
+import { BushListener } from '@lib';
+import { AutoMod } from '../../lib/common/automod';
import { BushClientEvents } from '../../lib/extensions/discord.js/BushClientEvents';
export default class AutomodMessageCreateListener extends BushListener {
@@ -18,107 +12,6 @@ export default class AutomodMessageCreateListener extends BushListener {
}
public override async exec(...[message]: BushClientEvents['messageCreate']): Promise<unknown> {
- return await AutomodMessageCreateListener.automod(message);
- }
-
- public static async automod(message: BushMessage): Promise<unknown> {
- if (message.channel.type === 'DM' || !message.guild) return;
- if (!(await message.guild.hasFeature('automod'))) return;
-
- const customAutomodPhrases = (await message.guild.getSetting('autoModPhases')) ?? {};
-
- const badLinks: { [key: string]: 0 | 1 | 2 | 3 } = {};
- let temp = _badLinks;
- if (_badLinksSecret) temp = temp.concat(_badLinksSecret);
-
- temp.forEach((link: string) => {
- badLinks[link] = 3;
- });
- const badWords: { [key: string]: 0 | 1 | 2 | 3 } = _badWords;
-
- const wordMap = { ...badWords, ...badLinks, ...customAutomodPhrases };
- const wordKeys = Object.keys(wordMap);
- const offences: { [key: string]: 0 | 1 | 2 | 3 } = {};
-
- const cleanMessageContent = message.content?.toLowerCase().replace(/ /g, '');
- for (const word of wordKeys) {
- const cleanWord = word.toLowerCase().replace(/ /g, '');
-
- if (cleanMessageContent.includes(cleanWord)) {
- if (cleanWord === 'whore' && !message.content?.toLowerCase().includes(cleanWord)) return;
- if (!offences[word]) offences[word] = wordMap[word as keyof typeof wordMap];
- }
- }
-
- if (!Object.keys(offences)?.length) return;
-
- const highestOffence = Object.values(offences).sort((a, b) => b - a)[0];
-
- switch (highestOffence) {
- case 0: {
- void message.delete().catch(() => {});
- break;
- }
- case 1: {
- void message.delete().catch(() => {});
- void message.member?.warn({
- moderator: message.guild.me!,
- reason: '[AutoMod] blacklisted word'
- });
-
- break;
- }
- case 2: {
- void message.delete().catch(() => {});
- void message.member?.mute({
- moderator: message.guild.me!,
- reason: '[AutoMod] blacklisted word',
- duration: 900_000 // 15 minutes
- });
- break;
- }
- case 3: {
- void message.delete().catch(() => {});
- void message.member?.mute({
- moderator: message.guild.me!,
- reason: '[AutoMod] blacklisted word',
- duration: 0 // perm
- });
- break;
- }
- }
-
- void client.console.info(
- 'autoMod',
- `Severity <<${highestOffence}>> action performed on <<${message.author.tag}>> (<<${message.author.id}>>) in <<#${message.channel.name}>> in <<${message.guild.name}>>`
- );
-
- const color =
- highestOffence === 0
- ? util.colors.lightGray
- : highestOffence === 1
- ? util.colors.yellow
- : highestOffence === 2
- ? util.colors.orange
- : util.colors.red;
-
- const automodChannel = await message.guild.getLogChannel('automod');
- if (!automodChannel) return;
-
- if (automodChannel.permissionsFor(message.guild.me!.id)?.has(['VIEW_CHANNEL', 'SEND_MESSAGES', 'EMBED_LINKS']))
- void automodChannel.send({
- embeds: [
- new MessageEmbed()
- .setTitle(`[Severity ${highestOffence}] Automod Action Performed`)
- .setDescription(
- `**User:** ${message.author} (${message.author.tag})\n**Sent From**: <#${message.channel.id}> [Jump to context](${
- message.url
- })\n**Blacklisted Words:** ${util.surroundArray(Object.keys(offences), '`').join(', ')}`
- )
- .addField('Message Content', `${await util.codeblock(message.content, 1024)}`)
- .setColor(color)
- .setTimestamp()
- ]
- });
+ return new AutoMod(message);
}
}
diff --git a/src/listeners/message/automodUpdate.ts b/src/listeners/message/automodUpdate.ts
index 9ef229e..86411b2 100644
--- a/src/listeners/message/automodUpdate.ts
+++ b/src/listeners/message/automodUpdate.ts
@@ -1,6 +1,6 @@
import { BushListener, BushMessage } from '@lib';
+import { AutoMod } from '../../lib/common/automod';
import { BushClientEvents } from '../../lib/extensions/discord.js/BushClientEvents';
-import AutomodMessageCreateListener from './automodCreate';
export default class AutomodMessageUpdateListener extends BushListener {
public constructor() {
@@ -13,6 +13,6 @@ export default class AutomodMessageUpdateListener extends BushListener {
public override async exec(...[_, newMessage]: BushClientEvents['messageUpdate']): Promise<unknown> {
const fullMessage = newMessage.partial ? await newMessage.fetch() : (newMessage as BushMessage);
- return await AutomodMessageCreateListener.automod(fullMessage);
+ return new AutoMod(fullMessage);
}
}
diff --git a/tsconfig.json b/tsconfig.json
index f3fa5ba..4495f03 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,7 +3,14 @@
"module": "commonjs",
"target": "esnext",
"outDir": "dist",
- "lib": ["esnext", "esnext.array", "esnext.asyncIterable", "esnext.intl", "esnext.symbol", "DOM"],
+ "lib": [
+ "esnext",
+ "esnext.array",
+ "esnext.asyncIterable",
+ "esnext.intl",
+ "esnext.symbol",
+ "DOM"
+ ],
"sourceMap": false,
"inlineSourceMap": true,
"inlineSources": true,
@@ -19,11 +26,21 @@
"baseUrl": "./",
"useUnknownInCatchVariables": false,
"paths": {
- "src/*": ["./src/*"],
- "@lib": ["./src/lib"],
- "@root": ["."]
+ "src/*": [
+ "./src/*"
+ ],
+ "@lib": [
+ "./src/lib"
+ ],
+ "@root": [
+ "."
+ ]
}
},
- "include": ["src/**/*.ts", "ecosystem.config.js"],
+ "include": [
+ "src/**/*.ts",
+ "lib/**/*.ts",
+ "ecosystem.config.js"
+ ],
"exclude": []
-}
+} \ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index a9926a4..6e14fc5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1066,13 +1066,13 @@ __metadata:
linkType: hard
"discord-akairo@npm:@notenoughupdates/discord-akairo@dev":
- version: 9.0.3-dev.1633868201.8b5b67e
- resolution: "@notenoughupdates/discord-akairo@npm:9.0.3-dev.1633868201.8b5b67e"
+ version: 9.0.5-dev.1633983022.8a77c5b
+ resolution: "@notenoughupdates/discord-akairo@npm:9.0.5-dev.1633983022.8a77c5b"
dependencies:
discord-akairo-message-util: "npm:@notenoughupdates/discord-akairo-message-util@latest"
lodash: ^4.17.21
source-map-support: ^0.5.20
- checksum: f196cdd5debca9752bbee703dcd878f14407ede1d12e0d5587c4eb0213a287b0582032d6d4482b59b749454165073ef807583c0d3b4dd8f81b9557b7c8a2ae86
+ checksum: b36461d1cb6232e2dc4f5f6a5ef93d68a92deed3305f18e39ab42ab6f50301783a70cd0e71f464dd62c6546a6ba08b4ae1ddba7a71f66a3391fc0ac9106130eb
languageName: node
linkType: hard
@@ -1098,8 +1098,8 @@ __metadata:
linkType: hard
"discord.js@npm:@notenoughupdates/discord.js@dev":
- version: 13.3.0-dev.1633867636.c462423
- resolution: "@notenoughupdates/discord.js@npm:13.3.0-dev.1633867636.c462423"
+ version: 13.3.0-dev.1633954072.c462423
+ resolution: "@notenoughupdates/discord.js@npm:13.3.0-dev.1633954072.c462423"
dependencies:
"@discordjs/builders": ^0.6.0
"@discordjs/collection": ^0.2.1
@@ -1110,7 +1110,7 @@ __metadata:
discord-api-types: ^0.23.1
node-fetch: ^2.6.1
ws: ^8.2.3
- checksum: 3f035abe71c6de7aabef20818f50c131b14dd2d979d0f505a3d0cb1406abd2887db342503140edba608afd26e4179597e61f0b032c50f453de94cd5dbffe2e22
+ checksum: ba59cf4327dafc73c88c0814b901dce3c3b45df50d6d85c362d3ec3bd902ee5414a9e11e79b25a471190b6645afbe64add7afaf38020a4b4d19156902c6b10bc
languageName: node
linkType: hard