aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintrc.cjs2
-rw-r--r--.gitignore1
-rw-r--r--.vscode/settings.json5
-rw-r--r--config/tsconfig.json7
-rw-r--r--lib/arguments/abbreviatedNumber.ts (renamed from src/arguments/abbreviatedNumber.ts)0
-rw-r--r--lib/arguments/contentWithDuration.ts (renamed from src/arguments/contentWithDuration.ts)0
-rw-r--r--lib/arguments/discordEmoji.ts (renamed from src/arguments/discordEmoji.ts)0
-rw-r--r--lib/arguments/duration.ts (renamed from src/arguments/duration.ts)0
-rw-r--r--lib/arguments/durationSeconds.ts (renamed from src/arguments/durationSeconds.ts)0
-rw-r--r--lib/arguments/globalUser.ts (renamed from src/arguments/globalUser.ts)0
-rw-r--r--lib/arguments/index.ts (renamed from src/arguments/index.ts)0
-rw-r--r--lib/arguments/messageLink.ts (renamed from src/arguments/messageLink.ts)0
-rw-r--r--lib/arguments/permission.ts (renamed from src/arguments/permission.ts)0
-rw-r--r--lib/arguments/roleWithDuration.ts (renamed from src/arguments/roleWithDuration.ts)0
-rw-r--r--lib/arguments/snowflake.ts (renamed from src/arguments/snowflake.ts)0
-rw-r--r--lib/arguments/tinyColor.ts (renamed from src/arguments/tinyColor.ts)0
-rw-r--r--lib/automod/AutomodShared.ts310
-rw-r--r--lib/automod/MemberAutomod.ts72
-rw-r--r--lib/automod/MessageAutomod.ts286
-rw-r--r--lib/automod/PresenceAutomod.ts85
-rw-r--r--lib/badlinks.ts (renamed from src/lib/badlinks.ts)0
-rw-r--r--lib/badwords.ts (renamed from src/lib/badwords.ts)95
-rw-r--r--lib/common/BushCache.ts (renamed from src/lib/utils/BushCache.ts)0
-rw-r--r--lib/common/ButtonPaginator.ts (renamed from src/lib/common/ButtonPaginator.ts)7
-rw-r--r--lib/common/CanvasProgressBar.ts (renamed from src/lib/utils/CanvasProgressBar.ts)0
-rw-r--r--lib/common/ConfirmationPrompt.ts (renamed from src/lib/common/ConfirmationPrompt.ts)0
-rw-r--r--lib/common/DeleteButton.ts (renamed from src/lib/common/DeleteButton.ts)0
-rw-r--r--lib/common/HighlightManager.ts (renamed from src/lib/common/HighlightManager.ts)5
-rw-r--r--lib/common/Moderation.ts (renamed from src/lib/common/util/Moderation.ts)0
-rw-r--r--lib/common/Sentry.ts (renamed from src/lib/common/Sentry.ts)2
-rw-r--r--lib/common/tags.ts (renamed from src/lib/common/tags.ts)0
-rw-r--r--lib/extensions/discord-akairo/BushArgumentTypeCaster.ts (renamed from src/lib/extensions/discord-akairo/BushArgumentTypeCaster.ts)0
-rw-r--r--lib/extensions/discord-akairo/BushClient.ts (renamed from src/lib/extensions/discord-akairo/BushClient.ts)36
-rw-r--r--lib/extensions/discord-akairo/BushCommand.ts (renamed from src/lib/extensions/discord-akairo/BushCommand.ts)0
-rw-r--r--lib/extensions/discord-akairo/BushCommandHandler.ts (renamed from src/lib/extensions/discord-akairo/BushCommandHandler.ts)0
-rw-r--r--lib/extensions/discord-akairo/BushInhibitor.ts (renamed from src/lib/extensions/discord-akairo/BushInhibitor.ts)0
-rw-r--r--lib/extensions/discord-akairo/BushInhibitorHandler.ts (renamed from src/lib/extensions/discord-akairo/BushInhibitorHandler.ts)0
-rw-r--r--lib/extensions/discord-akairo/BushListener.ts (renamed from src/lib/extensions/discord-akairo/BushListener.ts)0
-rw-r--r--lib/extensions/discord-akairo/BushListenerHandler.ts (renamed from src/lib/extensions/discord-akairo/BushListenerHandler.ts)0
-rw-r--r--lib/extensions/discord-akairo/BushTask.ts (renamed from src/lib/extensions/discord-akairo/BushTask.ts)0
-rw-r--r--lib/extensions/discord-akairo/BushTaskHandler.ts (renamed from src/lib/extensions/discord-akairo/BushTaskHandler.ts)0
-rw-r--r--lib/extensions/discord-akairo/SlashMessage.ts (renamed from src/lib/extensions/discord-akairo/SlashMessage.ts)0
-rw-r--r--lib/extensions/discord.js/BushClientEvents.ts (renamed from src/lib/extensions/discord.js/BushClientEvents.ts)0
-rw-r--r--lib/extensions/discord.js/ExtendedGuild.ts (renamed from src/lib/extensions/discord.js/ExtendedGuild.ts)7
-rw-r--r--lib/extensions/discord.js/ExtendedGuildMember.ts (renamed from src/lib/extensions/discord.js/ExtendedGuildMember.ts)0
-rw-r--r--lib/extensions/discord.js/ExtendedMessage.ts (renamed from src/lib/extensions/discord.js/ExtendedMessage.ts)2
-rw-r--r--lib/extensions/discord.js/ExtendedUser.ts (renamed from src/lib/extensions/discord.js/ExtendedUser.ts)0
-rw-r--r--lib/extensions/global.ts (renamed from src/lib/extensions/global.ts)0
-rw-r--r--lib/index.ts (renamed from src/lib/index.ts)21
-rw-r--r--lib/models/BaseModel.ts (renamed from src/lib/models/BaseModel.ts)2
-rw-r--r--lib/models/instance/ActivePunishment.ts (renamed from src/lib/models/instance/ActivePunishment.ts)0
-rw-r--r--lib/models/instance/Guild.ts (renamed from src/lib/models/instance/Guild.ts)13
-rw-r--r--lib/models/instance/Highlight.ts (renamed from src/lib/models/instance/Highlight.ts)0
-rw-r--r--lib/models/instance/Level.ts (renamed from src/lib/models/instance/Level.ts)0
-rw-r--r--lib/models/instance/ModLog.ts (renamed from src/lib/models/instance/ModLog.ts)0
-rw-r--r--lib/models/instance/Reminder.ts (renamed from src/lib/models/instance/Reminder.ts)0
-rw-r--r--lib/models/instance/StickyRole.ts (renamed from src/lib/models/instance/StickyRole.ts)0
-rw-r--r--lib/models/shared/Global.ts (renamed from src/lib/models/shared/Global.ts)0
-rw-r--r--lib/models/shared/GuildCount.ts (renamed from src/lib/models/shared/GuildCount.ts)5
-rw-r--r--lib/models/shared/MemberCount.ts (renamed from src/lib/models/shared/MemberCount.ts)3
-rw-r--r--lib/models/shared/Shared.ts (renamed from src/lib/models/shared/Shared.ts)2
-rw-r--r--lib/models/shared/Stat.ts (renamed from src/lib/models/shared/Stat.ts)0
-rw-r--r--lib/tsconfig.json9
-rw-r--r--lib/types/BushInspectOptions.ts (renamed from src/lib/common/typings/BushInspectOptions.ts)0
-rw-r--r--lib/types/CodeBlockLang.ts (renamed from src/lib/common/typings/CodeBlockLang.ts)0
-rw-r--r--lib/utils/AllowedMentions.ts (renamed from src/lib/utils/AllowedMentions.ts)0
-rw-r--r--lib/utils/Arg.ts (renamed from src/lib/common/util/Arg.ts)0
-rw-r--r--lib/utils/BushClientUtils.ts (renamed from src/lib/utils/BushClientUtils.ts)15
-rw-r--r--lib/utils/BushConstants.ts (renamed from src/lib/utils/BushConstants.ts)0
-rw-r--r--lib/utils/BushLogger.ts (renamed from src/lib/utils/BushLogger.ts)0
-rw-r--r--lib/utils/BushUtils.ts (renamed from src/lib/utils/BushUtils.ts)3
-rw-r--r--lib/utils/Format.ts (renamed from src/lib/common/util/Format.ts)0
-rw-r--r--lib/utils/Minecraft.ts (renamed from src/lib/common/util/Minecraft.ts)4
-rw-r--r--lib/utils/Minecraft_Test.ts (renamed from src/lib/common/util/Minecraft_Test.ts)0
-rw-r--r--misc/test.js (renamed from test.js)0
-rw-r--r--misc/test.png (renamed from test.png)bin41015 -> 41015 bytes
-rw-r--r--misc/tooltips.nnb (renamed from tooltips.nnb)0
-rw-r--r--package.json14
-rw-r--r--src/bot.ts6
-rw-r--r--src/commands/admin/channelPermissions.ts2
-rw-r--r--src/commands/dev/test.ts6
-rw-r--r--src/commands/info/help.ts2
-rw-r--r--src/commands/moderation/massEvidence.ts2
-rw-r--r--src/commands/moderation/myLogs.ts2
-rw-r--r--src/commands/moderation/unmute.ts2
-rw-r--r--src/commands/moulberry-bush/neuRepo.ts2
-rw-r--r--src/commands/moulberry-bush/rule.ts2
-rw-r--r--src/commands/utilities/calculator.ts2
-rw-r--r--src/commands/utilities/highlight-block.ts2
-rw-r--r--src/commands/utilities/highlight-unblock.ts2
-rw-r--r--src/commands/utilities/uuid.ts2
-rw-r--r--src/commands/utilities/wolframAlpha.ts2
-rw-r--r--src/context-menu-commands/message/viewRaw.ts2
-rw-r--r--src/context-menu-commands/user/modlog.ts2
-rw-r--r--src/context-menu-commands/user/userInfo.ts2
-rw-r--r--src/lib/common/AutoMod.ts529
-rw-r--r--src/listeners/automod/automodCreate.ts (renamed from src/listeners/message/automodCreate.ts)5
-rw-r--r--src/listeners/automod/automodUpdate.ts (renamed from src/listeners/message/automodUpdate.ts)6
-rw-r--r--src/listeners/automod/memberAutomod.ts21
-rw-r--r--src/listeners/automod/presenceAutomod.ts27
-rw-r--r--src/listeners/commands/commandError.ts4
-rw-r--r--src/listeners/interaction/interactionCreate.ts4
-rw-r--r--src/listeners/member-custom/bushLevelUpdate.ts6
-rw-r--r--src/tasks/cache/updateCache.ts4
-rw-r--r--src/tasks/cache/updateHighlightCache.ts4
-rw-r--r--src/tasks/cache/updatePriceItemCache.ts7
-rw-r--r--src/tasks/feature/handleReminders.ts3
-rw-r--r--src/tasks/stats/guildCount.ts2
-rw-r--r--src/tsconfig.json9
-rw-r--r--tests/arguments/abbreviatedNumber.test.ts4
-rw-r--r--tsconfig.base.json67
-rw-r--r--tsconfig.eslint.json19
-rw-r--r--tsconfig.json33
113 files changed, 1143 insertions, 666 deletions
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index f62abfd..d246897 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -131,7 +131,7 @@ module.exports = {
project: './tsconfig.eslint.json'
},
plugins: ['@typescript-eslint', 'deprecation', 'import'],
- ignorePatterns: ['dist'],
+ ignorePatterns: ['dist', 'node_modules'],
rules: {
'no-return-await': 'off',
'@typescript-eslint/no-empty-interface': 'warn',
diff --git a/.gitignore b/.gitignore
index 1ce2d89..2ec4b06 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,3 +33,4 @@ src/config/options.ts
config/options.ts
src/lib/badlinks-secret.ts
+lib/badlinks-secret.ts
diff --git a/.vscode/settings.json b/.vscode/settings.json
index bf4dd62..d86374b 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -6,9 +6,8 @@
"**/CVS": true,
"**/.DS_Store": true,
"dist": false,
- ".pnp.js": true,
- "**/node_modules": true,
- "**/dist/**/*.js.map": true
+ ".pnp.js": false,
+ "**/node_modules": true
},
"javascript.preferences.importModuleSpecifier": "project-relative",
"typescript.preferences.importModuleSpecifier": "project-relative",
diff --git a/config/tsconfig.json b/config/tsconfig.json
new file mode 100644
index 0000000..46b2d15
--- /dev/null
+++ b/config/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "../../dist/config"
+ },
+ "files": ["./Config.ts", "example-options.ts", "options.ts"]
+}
diff --git a/src/arguments/abbreviatedNumber.ts b/lib/arguments/abbreviatedNumber.ts
index a7d8ce5..a7d8ce5 100644
--- a/src/arguments/abbreviatedNumber.ts
+++ b/lib/arguments/abbreviatedNumber.ts
diff --git a/src/arguments/contentWithDuration.ts b/lib/arguments/contentWithDuration.ts
index 0efba39..0efba39 100644
--- a/src/arguments/contentWithDuration.ts
+++ b/lib/arguments/contentWithDuration.ts
diff --git a/src/arguments/discordEmoji.ts b/lib/arguments/discordEmoji.ts
index 92d6502..92d6502 100644
--- a/src/arguments/discordEmoji.ts
+++ b/lib/arguments/discordEmoji.ts
diff --git a/src/arguments/duration.ts b/lib/arguments/duration.ts
index 09dd3d5..09dd3d5 100644
--- a/src/arguments/duration.ts
+++ b/lib/arguments/duration.ts
diff --git a/src/arguments/durationSeconds.ts b/lib/arguments/durationSeconds.ts
index d8d6749..d8d6749 100644
--- a/src/arguments/durationSeconds.ts
+++ b/lib/arguments/durationSeconds.ts
diff --git a/src/arguments/globalUser.ts b/lib/arguments/globalUser.ts
index 4324aa9..4324aa9 100644
--- a/src/arguments/globalUser.ts
+++ b/lib/arguments/globalUser.ts
diff --git a/src/arguments/index.ts b/lib/arguments/index.ts
index eebf0a2..eebf0a2 100644
--- a/src/arguments/index.ts
+++ b/lib/arguments/index.ts
diff --git a/src/arguments/messageLink.ts b/lib/arguments/messageLink.ts
index c95e42d..c95e42d 100644
--- a/src/arguments/messageLink.ts
+++ b/lib/arguments/messageLink.ts
diff --git a/src/arguments/permission.ts b/lib/arguments/permission.ts
index 98bfe74..98bfe74 100644
--- a/src/arguments/permission.ts
+++ b/lib/arguments/permission.ts
diff --git a/src/arguments/roleWithDuration.ts b/lib/arguments/roleWithDuration.ts
index b97f205..b97f205 100644
--- a/src/arguments/roleWithDuration.ts
+++ b/lib/arguments/roleWithDuration.ts
diff --git a/src/arguments/snowflake.ts b/lib/arguments/snowflake.ts
index b98a20f..b98a20f 100644
--- a/src/arguments/snowflake.ts
+++ b/lib/arguments/snowflake.ts
diff --git a/src/arguments/tinyColor.ts b/lib/arguments/tinyColor.ts
index 148c078..148c078 100644
--- a/src/arguments/tinyColor.ts
+++ b/lib/arguments/tinyColor.ts
diff --git a/lib/automod/AutomodShared.ts b/lib/automod/AutomodShared.ts
new file mode 100644
index 0000000..5d031d0
--- /dev/null
+++ b/lib/automod/AutomodShared.ts
@@ -0,0 +1,310 @@
+import {
+ ActionRowBuilder,
+ ButtonBuilder,
+ ButtonInteraction,
+ ButtonStyle,
+ GuildMember,
+ Message,
+ PermissionFlagsBits,
+ Snowflake
+} from 'discord.js';
+import UnmuteCommand from '../../src/commands/moderation/unmute.js';
+import * as Moderation from '../common/Moderation.js';
+import { unmuteResponse } from '../extensions/discord.js/ExtendedGuildMember.js';
+import { colors, emojis } from '../utils/BushConstants.js';
+import * as Format from '../utils/Format.js';
+
+/**
+ * Handles shared auto moderation functionality.
+ */
+export abstract class Automod {
+ /**
+ * Whether or not a punishment has already been given to the user
+ */
+ protected punished = false;
+
+ /**
+ * @param member The guild member that the automod is checking
+ */
+ protected constructor(protected readonly member: GuildMember) {}
+
+ /**
+ * The user
+ */
+ protected get user() {
+ return this.member.user;
+ }
+
+ /**
+ * The client instance
+ */
+ protected get client() {
+ return this.member.client;
+ }
+
+ /**
+ * The guild member that the automod is checking
+ */
+ protected get guild() {
+ return this.member.guild;
+ }
+
+ /**
+ * Whether or not the member should be immune to auto moderation
+ */
+ protected get isImmune() {
+ if (this.member.user.isOwner()) return true;
+ if (this.member.guild.ownerId === this.member.id) return true;
+ if (this.member.permissions.has('Administrator')) return true;
+
+ return false;
+ }
+
+ protected buttons(userId: Snowflake, reason: string, undo = true): ActionRowBuilder<ButtonBuilder> {
+ const row = new ActionRowBuilder<ButtonBuilder>().addComponents([
+ new ButtonBuilder({
+ style: ButtonStyle.Danger,
+ label: 'Ban User',
+ customId: `automod;ban;${userId};${reason}`
+ })
+ ]);
+
+ if (undo) {
+ row.addComponents(
+ new ButtonBuilder({
+ style: ButtonStyle.Success,
+ label: 'Unmute User',
+ customId: `automod;unmute;${userId}`
+ })
+ );
+ }
+
+ return row;
+ }
+
+ protected logColor(severity: Severity) {
+ switch (severity) {
+ case Severity.DELETE:
+ return colors.lightGray;
+ case Severity.WARN:
+ return colors.yellow;
+ case Severity.TEMP_MUTE:
+ return colors.orange;
+ case Severity.PERM_MUTE:
+ return colors.red;
+ }
+ throw new Error(`Unknown severity: ${severity}`);
+ }
+
+ /**
+ * Checks if any of the words provided are in the message
+ * @param words The words to check for
+ * @returns The blacklisted words found in the message
+ */
+ protected checkWords(words: BadWordDetails[], str: string): BadWordDetails[] {
+ if (words.length === 0) return [];
+
+ const matchedWords: BadWordDetails[] = [];
+ for (const word of words) {
+ if (word.regex) {
+ if (new RegExp(word.match).test(this.format(word.match, word))) {
+ matchedWords.push(word);
+ }
+ } else {
+ if (this.format(str, word).includes(this.format(word.match, word))) {
+ matchedWords.push(word);
+ }
+ }
+ }
+ return matchedWords;
+ }
+
+ /**
+ * Format a string according to the word options
+ * @param string The string to format
+ * @param wordOptions The word options to format with
+ * @returns The formatted string
+ */
+ protected format(string: string, wordOptions: BadWordDetails) {
+ const temp = wordOptions.ignoreCapitalization ? string.toLowerCase() : string;
+ return wordOptions.ignoreSpaces ? temp.replace(/ /g, '') : temp;
+ }
+
+ /**
+ * Handles the auto moderation
+ */
+ protected abstract handle(): Promise<void>;
+}
+
+/**
+ * Handles the ban button in the automod log.
+ * @param interaction The button interaction.
+ */
+export async function handleAutomodInteraction(interaction: ButtonInteraction) {
+ if (!interaction.memberPermissions?.has(PermissionFlagsBits.BanMembers))
+ return interaction.reply({
+ content: `${emojis.error} You are missing the **Ban Members** permission.`,
+ ephemeral: true
+ });
+ const [action, userId, reason] = interaction.customId.replace('automod;', '').split(';') as ['ban' | 'unmute', string, string];
+
+ if (!(['ban', 'unmute'] as const).includes(action)) throw new TypeError(`Invalid automod button action: ${action}`);
+
+ const victim = await interaction.guild!.members.fetch(userId).catch(() => null);
+ const moderator =
+ interaction.member instanceof GuildMember ? interaction.member : await interaction.guild!.members.fetch(interaction.user.id);
+
+ switch (action) {
+ case 'ban': {
+ if (!interaction.guild?.members.me?.permissions.has('BanMembers'))
+ return interaction.reply({
+ content: `${emojis.error} I do not have permission to ${action} members.`,
+ ephemeral: true
+ });
+
+ const check = victim ? await Moderation.permissionCheck(moderator, victim, 'ban', true) : true;
+ if (check !== true) return interaction.reply({ content: check, ephemeral: true });
+
+ const result = await interaction.guild?.bushBan({
+ user: userId,
+ reason,
+ moderator: interaction.user.id,
+ evidence: (interaction.message as Message).url ?? undefined
+ });
+
+ const victimUserFormatted = (await interaction.client.utils.resolveNonCachedUser(userId))?.tag ?? userId;
+
+ const content = (() => {
+ if (result === unmuteResponse.SUCCESS) {
+ return `${emojis.success} Successfully banned ${Format.input(victimUserFormatted)}.`;
+ } else if (result === unmuteResponse.DM_ERROR) {
+ return `${emojis.warn} Banned ${Format.input(victimUserFormatted)} however I could not send them a dm.`;
+ } else {
+ return `${emojis.error} Could not ban ${Format.input(victimUserFormatted)}: \`${result}\` .`;
+ }
+ })();
+
+ return interaction.reply({
+ content: content,
+ ephemeral: true
+ });
+ }
+
+ case 'unmute': {
+ if (!victim)
+ return interaction.reply({
+ content: `${emojis.error} Cannot find member, they may have left the server.`,
+ ephemeral: true
+ });
+
+ if (!interaction.guild)
+ return interaction.reply({
+ content: `${emojis.error} This is weird, I don't seem to be in the server...`,
+ ephemeral: true
+ });
+
+ const check = await Moderation.permissionCheck(moderator, victim, 'unmute', true);
+ if (check !== true) return interaction.reply({ content: check, ephemeral: true });
+
+ const check2 = await Moderation.checkMutePermissions(interaction.guild);
+ if (check2 !== true) return interaction.reply({ content: UnmuteCommand.formatCode('/', victim!, check2), ephemeral: true });
+
+ const result = await victim.bushUnmute({
+ reason,
+ moderator: interaction.member as GuildMember,
+ evidence: (interaction.message as Message).url ?? undefined
+ });
+
+ const victimUserFormatted = victim.user.tag;
+
+ const content = (() => {
+ if (result === unmuteResponse.SUCCESS) {
+ return `${emojis.success} Successfully unmuted ${Format.input(victimUserFormatted)}.`;
+ } else if (result === unmuteResponse.DM_ERROR) {
+ return `${emojis.warn} Unmuted ${Format.input(victimUserFormatted)} however I could not send them a dm.`;
+ } else {
+ return `${emojis.error} Could not unmute ${Format.input(victimUserFormatted)}: \`${result}\` .`;
+ }
+ })();
+
+ return interaction.reply({
+ content: content,
+ ephemeral: true
+ });
+ }
+ }
+}
+
+/**
+ * The severity of the blacklisted word
+ */
+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
+}
+
+/**
+ * Details about a blacklisted word
+ */
+export interface BadWordDetails {
+ /**
+ * The word that is blacklisted
+ */
+ match: string;
+
+ /**
+ * The severity of the word
+ */
+ severity: Severity | 1 | 2 | 3;
+
+ /**
+ * Whether or not to ignore spaces when checking for the word
+ */
+ ignoreSpaces: boolean;
+
+ /**
+ * Whether or not to ignore case when checking for the word
+ */
+ ignoreCapitalization: boolean;
+
+ /**
+ * The reason that this word is blacklisted (used for the punishment reason)
+ */
+ reason: string;
+
+ /**
+ * Whether or not the word is regex
+ * @default false
+ */
+ regex: boolean;
+
+ /**
+ * Whether to also check a user's status and username for the phrase
+ * @default false
+ */
+ userInfo: boolean;
+}
+
+/**
+ * Blacklisted words mapped to their details
+ */
+export interface BadWords {
+ [category: string]: BadWordDetails[];
+}
diff --git a/lib/automod/MemberAutomod.ts b/lib/automod/MemberAutomod.ts
new file mode 100644
index 0000000..6f71457
--- /dev/null
+++ b/lib/automod/MemberAutomod.ts
@@ -0,0 +1,72 @@
+import { stripIndent } from '#tags';
+import { EmbedBuilder, GuildMember } from 'discord.js';
+import { Automod, BadWordDetails } from './AutomodShared.js';
+
+export class MemberAutomod extends Automod {
+ /**
+ * @param member The member that the automod is checking
+ */
+ public constructor(member: GuildMember) {
+ super(member);
+
+ if (member.id === member.client.user?.id) return;
+
+ void this.handle();
+ }
+
+ protected async handle(): Promise<void> {
+ if (this.member.user.bot) return;
+
+ const badWordsRaw = Object.values(this.client.utils.getShared('badWords')).flat();
+ const customAutomodPhrases = (await this.guild.getSetting('autoModPhases')) ?? [];
+
+ const phrases = [...badWordsRaw, ...customAutomodPhrases].filter((p) => p.userInfo);
+
+ const result: BadWordDetails[] = [];
+
+ const str = `${this.member.user.username}${this.member.nickname ? `\n${this.member.nickname}` : ''}`;
+ const check = this.checkWords(phrases, str);
+ if (check.length > 0) {
+ result.push(...check);
+ }
+
+ if (result.length > 0) {
+ const highestOffense = result.sort((a, b) => b.severity - a.severity)[0];
+ await this.logMessage(highestOffense, result, str);
+ }
+ }
+
+ /**
+ * Log an automod infraction to the guild's specified automod log channel
+ * @param highestOffense The highest severity word found in the message
+ * @param offenses The other offenses that were also matched in the message
+ */
+ protected async logMessage(highestOffense: BadWordDetails, offenses: BadWordDetails[], str: string) {
+ void this.client.console.info(
+ 'MemberAutomod',
+ `Detected a severity <<${highestOffense.severity}>> automod phrase in <<${this.user.tag}>>'s (<<${this.user.id}>>) username or nickname in <<${this.guild.name}>>`
+ );
+
+ const color = this.logColor(highestOffense.severity);
+
+ await this.guild.sendLogChannel('automod', {
+ embeds: [
+ new EmbedBuilder()
+ .setTitle(`[Severity ${highestOffense.severity}] Automoderated User Info Detected`)
+ .setDescription(
+ stripIndent`
+ **User:** ${this.user} (${this.user.tag})
+ **Blacklisted Words:** ${offenses.map((o) => `\`${o.match}\``).join(', ')}`
+ )
+ .addFields({
+ name: 'Info',
+ value: `${await this.client.utils.codeblock(str, 1024)}`
+ })
+ .setColor(color)
+ .setTimestamp()
+ .setAuthor({ name: this.user.tag, url: this.user.displayAvatarURL() })
+ ],
+ components: [this.buttons(this.user.id, highestOffense.reason, false)]
+ });
+ }
+}
diff --git a/lib/automod/MessageAutomod.ts b/lib/automod/MessageAutomod.ts
new file mode 100644
index 0000000..9673adf
--- /dev/null
+++ b/lib/automod/MessageAutomod.ts
@@ -0,0 +1,286 @@
+import { stripIndent } from '#tags';
+import assert from 'assert/strict';
+import chalk from 'chalk';
+import { EmbedBuilder, GuildTextBasedChannel, PermissionFlagsBits, type Message } from 'discord.js';
+import { colors } from '../utils/BushConstants.js';
+import { format, formatError } from '../utils/BushUtils.js';
+import { Automod, BadWordDetails, Severity } from './AutomodShared.js';
+
+/**
+ * Handles message auto moderation functionality.
+ */
+export class MessageAutomod extends Automod {
+ /**
+ * @param message The message to check and potentially perform automod actions on
+ */
+ public constructor(private readonly message: Message) {
+ assert(message.member);
+ super(message.member);
+
+ if (message.author.id === message.client.user?.id) return;
+ void this.handle();
+ }
+
+ /**
+ * Handles the auto moderation
+ */
+ protected async handle() {
+ if (!this.message.inGuild()) return;
+ if (!(await this.guild.hasFeature('automod'))) return;
+ if (this.user.bot) return;
+ if (!this.message.member) return;
+
+ traditional: {
+ if (this.isImmune) break traditional;
+ const badLinksArray = this.client.utils.getShared('badLinks');
+ const badLinksSecretArray = this.client.utils.getShared('badLinksSecret');
+ const badWordsRaw = this.client.utils.getShared('badWords');
+
+ const customAutomodPhrases = (await this.guild.getSetting('autoModPhases')) ?? [];
+ const uniqueLinks = [...new Set([...badLinksArray, ...badLinksSecretArray])];
+
+ const badLinks: BadWordDetails[] = uniqueLinks.map((link) => ({
+ match: link,
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: false,
+ ignoreCapitalization: true,
+ reason: 'malicious link',
+ regex: false,
+ userInfo: false
+ }));
+
+ const parsedBadWords = Object.values(badWordsRaw).flat();
+
+ const result = this.checkWords(
+ [
+ ...customAutomodPhrases,
+ ...((await this.guild.hasFeature('excludeDefaultAutomod')) ? [] : parsedBadWords),
+ ...((await this.guild.hasFeature('excludeAutomodScamLinks')) ? [] : badLinks)
+ ],
+ this.message.content
+ );
+
+ if (result.length === 0) break traditional;
+
+ const highestOffense = result.sort((a, b) => b.severity - a.severity)[0];
+
+ if (highestOffense.severity === undefined || highestOffense.severity === null) {
+ void this.guild.sendLogChannel('error', {
+ embeds: [
+ {
+ title: 'AutoMod Error',
+ description: `Unable to find severity information for ${format.inlineCode(highestOffense.match)}`,
+ color: colors.error
+ }
+ ]
+ });
+ } else {
+ this.punish(highestOffense);
+ void this.logMessage(highestOffense, result);
+ }
+ }
+
+ other: {
+ if (this.isImmune) break other;
+ if (!this.punished && (await this.guild.hasFeature('delScamMentions'))) void this.checkScamMentions();
+ }
+
+ if (!this.punished && (await this.guild.hasFeature('perspectiveApi'))) void this.checkPerspectiveApi();
+ }
+
+ /**
+ * If the message contains '@everyone' or '@here' and it contains a common scam phrase, it will be deleted
+ * @returns
+ */
+ protected async checkScamMentions() {
+ const includes = (c: string) => this.message.content.toLocaleLowerCase().includes(c);
+ if (!includes('@everyone') && !includes('@here')) return;
+
+ // It would be bad if we deleted a message that actually pinged @everyone or @here
+ if (
+ this.member.permissionsIn(this.message.channelId).has(PermissionFlagsBits.MentionEveryone) ||
+ this.message.mentions.everyone
+ )
+ return;
+
+ if (
+ includes('steam') ||
+ includes('www.youtube.com') ||
+ includes('youtu.be') ||
+ includes('nitro') ||
+ includes('1 month') ||
+ includes('3 months') ||
+ includes('personalize your profile') ||
+ includes('even more') ||
+ includes('xbox and discord') ||
+ includes('left over') ||
+ includes('check this lol') ||
+ includes('airdrop')
+ ) {
+ const color = this.logColor(Severity.PERM_MUTE);
+ this.punish({ severity: Severity.TEMP_MUTE, reason: 'everyone mention and scam phrase' } as BadWordDetails);
+ void this.guild!.sendLogChannel('automod', {
+ embeds: [
+ new EmbedBuilder()
+ .setTitle(`[Severity ${Severity.TEMP_MUTE}] Mention Scam Deleted`)
+ .setDescription(
+ stripIndent`
+ **User:** ${this.user} (${this.user.tag})
+ **Sent From:** <#${this.message.channel.id}> [Jump to context](${this.message.url})`
+ )
+ .addFields({
+ name: 'Message Content',
+ value: `${await this.client.utils.codeblock(this.message.content, 1024)}`
+ })
+ .setColor(color)
+ .setTimestamp()
+ ],
+ components: [this.buttons(this.user.id, 'everyone mention and scam phrase')]
+ });
+ }
+ }
+
+ protected async checkPerspectiveApi() {
+ return;
+ if (!this.client.config.isDevelopment) return;
+
+ if (!this.message.content) return;
+ this.client.perspective.comments.analyze(
+ {
+ key: this.client.config.credentials.perspectiveApiKey,
+ resource: {
+ comment: {
+ text: this.message.content
+ },
+ requestedAttributes: {
+ TOXICITY: {},
+ SEVERE_TOXICITY: {},
+ IDENTITY_ATTACK: {},
+ INSULT: {},
+ PROFANITY: {},
+ THREAT: {},
+ SEXUALLY_EXPLICIT: {},
+ FLIRTATION: {}
+ }
+ }
+ },
+ (err: any, response: any) => {
+ if (err) return console.log(err?.message);
+
+ const normalize = (val: number, min: number, max: number) => (val - min) / (max - min);
+
+ const color = (val: number) => {
+ if (val >= 0.5) {
+ const x = 194 - Math.round(normalize(val, 0.5, 1) * 194);
+ return chalk.rgb(194, x, 0)(val);
+ } else {
+ const x = Math.round(normalize(val, 0, 0.5) * 194);
+ return chalk.rgb(x, 194, 0)(val);
+ }
+ };
+
+ console.log(chalk.cyan(this.message.content));
+ Object.entries(response.data.attributeScores)
+ .sort(([a], [b]) => a.localeCompare(b))
+ .forEach(([key, value]: any[]) => console.log(chalk.white(key), color(value.summaryScore.value)));
+ }
+ );
+ }
+
+ /**
+ * Punishes the user based on the severity of the offense
+ * @param highestOffense The highest offense to punish the user for
+ * @returns The color of the embed that the log should, based on the severity of the offense
+ */
+ protected punish(highestOffense: BadWordDetails) {
+ switch (highestOffense.severity) {
+ case Severity.DELETE: {
+ void this.message.delete().catch((e) => deleteError.bind(this, e));
+ this.punished = true;
+ break;
+ }
+ case Severity.WARN: {
+ void this.message.delete().catch((e) => deleteError.bind(this, e));
+ void this.member.bushWarn({
+ moderator: this.guild!.members.me!,
+ reason: `[Automod] ${highestOffense.reason}`
+ });
+ this.punished = true;
+ break;
+ }
+ case Severity.TEMP_MUTE: {
+ void this.message.delete().catch((e) => deleteError.bind(this, e));
+ void this.member.bushMute({
+ moderator: this.guild!.members.me!,
+ reason: `[Automod] ${highestOffense.reason}`,
+ duration: 900_000 // 15 minutes
+ });
+ this.punished = true;
+ break;
+ }
+ case Severity.PERM_MUTE: {
+ void this.message.delete().catch((e) => deleteError.bind(this, e));
+ void this.member.bushMute({
+ moderator: this.guild!.members.me!,
+ reason: `[Automod] ${highestOffense.reason}`,
+ duration: 0 // permanent
+ });
+ this.punished = true;
+ break;
+ }
+ default: {
+ throw new Error(`Invalid severity: ${highestOffense.severity}`);
+ }
+ }
+
+ async function deleteError(this: MessageAutomod, e: Error | any) {
+ void this.guild?.sendLogChannel('error', {
+ embeds: [
+ {
+ title: 'Automod Error',
+ description: `Unable to delete triggered message.`,
+ fields: [{ name: 'Error', value: await this.client.utils.codeblock(`${formatError(e)}`, 1024, 'js', true) }],
+ color: colors.error
+ }
+ ]
+ });
+ }
+ }
+
+ /**
+ * Log an automod infraction to the guild's specified automod log channel
+ * @param highestOffense The highest severity word found in the message
+ * @param offenses The other offenses that were also matched in the message
+ */
+ protected async logMessage(highestOffense: BadWordDetails, offenses: BadWordDetails[]) {
+ void this.client.console.info(
+ 'MessageAutomod',
+ `Severity <<${highestOffense.severity}>> action performed on <<${this.user.tag}>> (<<${this.user.id}>>) in <<#${
+ (this.message.channel as GuildTextBasedChannel).name
+ }>> in <<${this.guild!.name}>>`
+ );
+
+ const color = this.logColor(highestOffense.severity);
+
+ await this.guild!.sendLogChannel('automod', {
+ embeds: [
+ new EmbedBuilder()
+ .setTitle(`[Severity ${highestOffense.severity}] Automod Action Performed`)
+ .setDescription(
+ stripIndent`
+ **User:** ${this.user} (${this.user.tag})
+ **Sent From:** <#${this.message.channel.id}> [Jump to context](${this.message.url})
+ **Blacklisted Words:** ${offenses.map((o) => `\`${o.match}\``).join(', ')}`
+ )
+ .addFields({
+ name: 'Message Content',
+ value: `${await this.client.utils.codeblock(this.message.content, 1024)}`
+ })
+ .setColor(color)
+ .setTimestamp()
+ .setAuthor({ name: this.user.tag, url: this.user.displayAvatarURL() })
+ ],
+ components: highestOffense.severity >= 2 ? [this.buttons(this.user.id, highestOffense.reason)] : undefined
+ });
+ }
+}
diff --git a/lib/automod/PresenceAutomod.ts b/lib/automod/PresenceAutomod.ts
new file mode 100644
index 0000000..70c66d6
--- /dev/null
+++ b/lib/automod/PresenceAutomod.ts
@@ -0,0 +1,85 @@
+import { stripIndent } from '#tags';
+import { EmbedBuilder, Presence } from 'discord.js';
+import { Automod, BadWordDetails } from './AutomodShared.js';
+
+export class PresenceAutomod extends Automod {
+ /**
+ * @param presence The presence that the automod is checking
+ */
+ public constructor(public readonly presence: Presence) {
+ super(presence.member!);
+
+ if (presence.member!.id === presence.client.user?.id) return;
+
+ void this.handle();
+ }
+
+ protected async handle(): Promise<void> {
+ if (this.presence.member!.user.bot) return;
+
+ const badWordsRaw = Object.values(this.client.utils.getShared('badWords')).flat();
+ const customAutomodPhrases = (await this.guild.getSetting('autoModPhases')) ?? [];
+
+ const phrases = [...badWordsRaw, ...customAutomodPhrases].filter((p) => p.userInfo);
+
+ const result: BadWordDetails[] = [];
+
+ const strings = [];
+
+ for (const activity of this.presence.activities) {
+ const str = `${activity.name}${activity.details ? `\n${activity.details}` : ''}${
+ activity.buttons.length > 0 ? `\n${activity.buttons.join('\n')}` : ''
+ }`;
+ const check = this.checkWords(phrases, str);
+ if (check.length > 0) {
+ result.push(...check);
+ strings.push(str);
+ }
+ }
+
+ if (result.length > 0) {
+ const highestOffense = result.sort((a, b) => b.severity - a.severity)[0];
+ await this.logMessage(highestOffense, result, strings);
+ }
+ }
+
+ /**
+ * Log an automod infraction to the guild's specified automod log channel
+ * @param highestOffense The highest severity word found in the message
+ * @param offenses The other offenses that were also matched in the message
+ */
+ protected async logMessage(highestOffense: BadWordDetails, offenses: BadWordDetails[], strings: string[]) {
+ void this.client.console.info(
+ 'PresenceAutomod',
+ `Detected a severity <<${highestOffense.severity}>> automod phrase in <<${this.user.tag}>>'s (<<${this.user.id}>>) presence in <<${this.guild.name}>>`
+ );
+
+ const color = this.logColor(highestOffense.severity);
+
+ await this.guild.sendLogChannel('automod', {
+ embeds: [
+ new EmbedBuilder()
+ .setTitle(`[Severity ${highestOffense.severity}] Automoderated Status Detected`)
+ .setDescription(
+ stripIndent`
+ **User:** ${this.user} (${this.user.tag})
+ **Blacklisted Words:** ${offenses.map((o) => `\`${o.match}\``).join(', ')}`
+ )
+ .addFields(
+ (
+ await Promise.all(
+ strings.map(async (s) => ({
+ name: 'Status',
+ value: `${await this.client.utils.codeblock(s, 1024)}`
+ }))
+ )
+ ).slice(0, 25)
+ )
+ .setColor(color)
+ .setTimestamp()
+ .setAuthor({ name: this.user.tag, url: this.user.displayAvatarURL() })
+ ],
+ components: [this.buttons(this.user.id, highestOffense.reason, false)]
+ });
+ }
+}
diff --git a/src/lib/badlinks.ts b/lib/badlinks.ts
index 3b4cf3b..3b4cf3b 100644
--- a/src/lib/badlinks.ts
+++ b/lib/badlinks.ts
diff --git a/src/lib/badwords.ts b/lib/badwords.ts
index feb74cb..5260264 100644
--- a/src/lib/badwords.ts
+++ b/lib/badwords.ts
@@ -1,5 +1,10 @@
-import type { BadWords } from "./common/AutoMod.js";
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import type { BadWords, Severity as AutomodSeverity } from "./automod/AutomodShared.js";
+// duplicated here so that this file can be compiled using the `isolatedModules` option
+/**
+ * @see {@link AutomodSeverity}
+ */
const enum Severity {
DELETE,
WARN,
@@ -19,6 +24,7 @@ export default {
ignoreCapitalization: true,
reason: "homophobic slur",
regex: false,
+ userInfo: true,
},
{
match: "nigga",
@@ -27,6 +33,7 @@ export default {
ignoreCapitalization: true,
reason: "racial slur",
regex: false,
+ userInfo: true,
},
{
match: "nigger",
@@ -35,6 +42,7 @@ export default {
ignoreCapitalization: true,
reason: "racial slur",
regex: false,
+ userInfo: true,
},
{
match: "nigra",
@@ -43,6 +51,7 @@ export default {
ignoreCapitalization: true,
reason: "racial slur",
regex: false,
+ userInfo: false,
},
{
match: "retard",
@@ -51,6 +60,7 @@ export default {
ignoreCapitalization: true,
reason: "ableist slur",
regex: false,
+ userInfo: true,
},
{
match: "retarted",
@@ -59,6 +69,7 @@ export default {
ignoreCapitalization: true,
reason: "ableist slur",
regex: false,
+ userInfo: false,
},
{
match: "slut",
@@ -67,6 +78,7 @@ export default {
ignoreCapitalization: true,
reason: "derogatory term",
regex: false,
+ userInfo: false,
},
{
match: "tar baby",
@@ -83,6 +95,7 @@ export default {
ignoreCapitalization: true,
reason: "derogatory term",
regex: false,
+ userInfo: false,
},
{
match: "卍",
@@ -91,6 +104,7 @@ export default {
ignoreCapitalization: true,
reason: "racist symbol",
regex: false,
+ userInfo: true,
},
{
//? N word
@@ -100,6 +114,7 @@ export default {
ignoreCapitalization: true,
reason: "racial slur",
regex: false,
+ userInfo: false,
},
{
//? N word
@@ -109,6 +124,7 @@ export default {
ignoreCapitalization: true,
reason: "racial slur",
regex: false,
+ userInfo: true,
},
],
@@ -124,6 +140,7 @@ export default {
ignoreCapitalization: true,
reason: "steam scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "hello i am leaving cs:go",
@@ -132,6 +149,7 @@ export default {
ignoreCapitalization: true,
reason: "steam scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "hello! I'm done with csgo",
@@ -140,6 +158,7 @@ export default {
ignoreCapitalization: true,
reason: "steam scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "hi bro, i'm leaving this fucking game, take my skin",
@@ -148,6 +167,7 @@ export default {
ignoreCapitalization: true,
reason: "steam scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "hi friend, today i am leaving this fucking game",
@@ -156,6 +176,7 @@ export default {
ignoreCapitalization: true,
reason: "steam scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "hi guys, i'm leaving this fucking game, take my",
@@ -164,6 +185,7 @@ export default {
ignoreCapitalization: true,
reason: "steam scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "hi, bro h am leaving cs:go and giving away my skin",
@@ -172,6 +194,7 @@ export default {
ignoreCapitalization: true,
reason: "steam scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "hi, bro i am leaving cs:go and giving away my skin",
@@ -180,6 +203,7 @@ export default {
ignoreCapitalization: true,
reason: "steam scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "i confirm all exchanges, there won't be enough",
@@ -188,6 +212,7 @@ export default {
ignoreCapitalization: true,
reason: "steam scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "i quit csgo",
@@ -196,6 +221,7 @@ export default {
ignoreCapitalization: true,
reason: "steam scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "the first three who send a trade",
@@ -204,6 +230,7 @@ export default {
ignoreCapitalization: true,
reason: "steam scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "you can choose any skin for yourself",
@@ -212,6 +239,7 @@ export default {
ignoreCapitalization: true,
reason: "steam scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "Hey, I'm leaving for the army and giving the skins",
@@ -220,6 +248,7 @@ export default {
ignoreCapitalization: true,
reason: "steam scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "fuck this trash called CS:GO, deleted,",
@@ -228,6 +257,7 @@ export default {
ignoreCapitalization: true,
reason: "steam scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "please take my skins",
@@ -236,6 +266,7 @@ export default {
ignoreCapitalization: true,
reason: "steam scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "Hi, I stopped playing CS:GO and decided to giveaway my inventory.",
@@ -244,6 +275,7 @@ export default {
ignoreCapitalization: true,
reason: "steam scam phrase",
regex: false,
+ userInfo: false,
},
],
@@ -258,6 +290,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "discord nitro for free - steam store",
@@ -266,6 +299,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "free 3 months of discord nitro",
@@ -274,6 +308,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "free discord nitro airdrop",
@@ -282,6 +317,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "get 3 months of discord nitro",
@@ -290,6 +326,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "get discord nitro for free",
@@ -298,6 +335,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "get free discord nitro from steam",
@@ -306,6 +344,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "lol, jahjajha free discord nitro for 3 month!!",
@@ -314,6 +353,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "steam is giving away 3 months of discord nitro for free to all no limited steam users",
@@ -322,6 +362,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
//? Lol, 1 month free discord nitro!
@@ -331,6 +372,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "Airdrop Discord FREE NITRO from Steam —",
@@ -339,6 +381,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "take nitro faster, it's already running out",
@@ -347,6 +390,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "only the first 10 people will have time to take nitro",
@@ -355,6 +399,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "Discord is giving away nitro!",
@@ -363,6 +408,7 @@ export default {
ignoreCapitalization: false,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "Free gift discord nitro for 1 month!",
@@ -371,6 +417,7 @@ export default {
ignoreCapitalization: false,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "Hi i claim this nitro for free 3 months lol!",
@@ -379,6 +426,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "bro watch this, working nitro gen",
@@ -387,6 +435,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "Free distribution of discord nitro for 3 months from steam!",
@@ -395,6 +444,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "Get 3 Months of Discord Nitro. Personalize your profile, screen share in HD, upgrade your emojis, and more!",
@@ -403,6 +453,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "Steam is giving away free discord nitro, have time to pick up at my link",
@@ -411,6 +462,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "Airdrop Discord NITRO with",
@@ -419,6 +471,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "Check this lol, there nitro is handed out for free, take it until everything is sorted out",
@@ -427,6 +480,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "A free Discord Nitro | Steam Store Discord Nitro Distribution.",
@@ -435,6 +489,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "Xbox gives away discord nitro for free",
@@ -443,6 +498,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "airdrop discord nitro by steam",
@@ -451,6 +507,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
//? 3 months nitro free from steam, take too
@@ -460,14 +517,17 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
+ // ? includes non-latin characters
match: "Free distributiοn of discοrd nitrο for 3 months from steаm!",
severity: Severity.PERM_MUTE,
ignoreSpaces: true,
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "Free discord nitro for 1 month!",
@@ -476,6 +536,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "I got some nitro left over here",
@@ -484,6 +545,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "Hey, steam gived nitro",
@@ -492,6 +554,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "nitro giveaway by steam, take it",
@@ -500,6 +563,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "3 months nitro from styme,",
@@ -508,6 +572,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "XBOX and DISCORD are giving away free NITRO FULL for a month.",
@@ -516,6 +581,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "Hi,take the Discord Nitro for free",
@@ -524,6 +590,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
//? Discord nitro got free, take it before it's too late
@@ -533,6 +600,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "1 month nitro for free",
@@ -541,6 +609,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "Gifts for the new year, nitro for 3 months",
@@ -549,6 +618,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "1 month nitro from steam, take it guys",
@@ -557,6 +627,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "Hello, discord and steam are giving away nitro, take it away",
@@ -565,6 +636,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "Who is first? :)",
@@ -573,6 +645,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "Whо is first? :)",
@@ -582,6 +655,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "Discord Nitro distribution from STEAM",
@@ -590,6 +664,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "3 month nitro for free, take it ",
@@ -598,6 +673,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "3 months nitro from steam, take it guys)",
@@ -606,6 +682,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "Gifts from steam nitro, gifts for 3 months",
@@ -614,6 +691,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "Free subscription for 3 months DISCORD NITRO",
@@ -622,6 +700,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "who will catch this gift?)",
@@ -630,6 +709,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "take it guys :)",
@@ -638,6 +718,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "Discord and Steam are giving away a free 3-month Discord Gift subscription!",
@@ -646,6 +727,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "Discord free nitro from steam",
@@ -654,6 +736,7 @@ export default {
ignoreCapitalization: true,
reason: "discord nitro scam phrase",
regex: false,
+ userInfo: false,
},
],
@@ -668,6 +751,7 @@ export default {
ignoreCapitalization: true,
reason: "misc. scam phrase",
regex: false,
+ userInfo: false,
},
{
match:
@@ -677,6 +761,7 @@ export default {
ignoreCapitalization: true,
reason: "annoying copy pasta",
regex: false,
+ userInfo: false,
},
{
match: "i made a game can you test play ?",
@@ -685,6 +770,7 @@ export default {
ignoreCapitalization: true,
reason: "malware phrase",
regex: false,
+ userInfo: false,
},
{
match: "tell me if something is wrong in the game",
@@ -693,6 +779,7 @@ export default {
ignoreCapitalization: true,
reason: "malware phrase",
regex: false,
+ userInfo: false,
},
{
match: "Hi, can you check out the game I created today:)",
@@ -701,6 +788,7 @@ export default {
ignoreCapitalization: true,
reason: "malware phrase",
regex: false,
+ userInfo: false,
},
{
match: "Just want to get other people's opinions, what to add and what to remove.",
@@ -709,6 +797,7 @@ export default {
ignoreCapitalization: true,
reason: "malware phrase",
regex: false,
+ userInfo: false,
},
{
match: "https://discord.gg/KKnGGvEPVM",
@@ -717,6 +806,7 @@ export default {
ignoreCapitalization: true,
reason: "misc. scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "https://discord.gg/rykjvpTGrB",
@@ -725,6 +815,7 @@ export default {
ignoreCapitalization: true,
reason: "misc. scam phrase",
regex: false,
+ userInfo: false,
},
{
match: "https://discord.gg/XTDQgJ9YMp",
@@ -733,6 +824,7 @@ export default {
ignoreCapitalization: true,
reason: "misc. scam phrase",
regex: false,
+ userInfo: false,
},
],
@@ -747,6 +839,7 @@ export default {
ignoreCapitalization: true,
reason: "advertising",
regex: false,
+ userInfo: false,
},
],
} as BadWords;
diff --git a/src/lib/utils/BushCache.ts b/lib/common/BushCache.ts
index 22a13ef..22a13ef 100644
--- a/src/lib/utils/BushCache.ts
+++ b/lib/common/BushCache.ts
diff --git a/src/lib/common/ButtonPaginator.ts b/lib/common/ButtonPaginator.ts
index 02c78ea..92f3796 100644
--- a/src/lib/common/ButtonPaginator.ts
+++ b/lib/common/ButtonPaginator.ts
@@ -102,7 +102,12 @@ export class ButtonPaginator {
} else {
await interaction
?.update({
- content: `${this.text ? `${this.text}\n` : ''}Command closed by user.`,
+ content: `${
+ this.text
+ ? `${this.text}
+`
+ : ''
+ }Command closed by user.`,
embeds: [],
components: []
})
diff --git a/src/lib/utils/CanvasProgressBar.ts b/lib/common/CanvasProgressBar.ts
index fb4f778..fb4f778 100644
--- a/src/lib/utils/CanvasProgressBar.ts
+++ b/lib/common/CanvasProgressBar.ts
diff --git a/src/lib/common/ConfirmationPrompt.ts b/lib/common/ConfirmationPrompt.ts
index b87d9ef..b87d9ef 100644
--- a/src/lib/common/ConfirmationPrompt.ts
+++ b/lib/common/ConfirmationPrompt.ts
diff --git a/src/lib/common/DeleteButton.ts b/lib/common/DeleteButton.ts
index 340d07f..340d07f 100644
--- a/src/lib/common/DeleteButton.ts
+++ b/lib/common/DeleteButton.ts
diff --git a/src/lib/common/HighlightManager.ts b/lib/common/HighlightManager.ts
index 4f891b7..cc31413 100644
--- a/src/lib/common/HighlightManager.ts
+++ b/lib/common/HighlightManager.ts
@@ -11,7 +11,7 @@ import {
type TextBasedChannel
} from 'discord.js';
import { colors, Time } from '../utils/BushConstants.js';
-import { sanitizeInputForDiscord } from './util/Format.js';
+import { sanitizeInputForDiscord } from '../utils/Format.js';
const NOTIFY_COOLDOWN = 5 * Time.Minute;
const OWNER_NOTIFY_COOLDOWN = 5 * Time.Minute;
@@ -28,6 +28,8 @@ type lastDM = Message;
type lastDmInfo = [lastDM: lastDM, guild: guild, channel: Snowflake, highlights: HighlightWord[]];
export class HighlightManager {
+ public static keep = new Set<Snowflake>();
+
/**
* Cached guild highlights.
*/
@@ -469,6 +471,7 @@ export class HighlightManager {
).get(message.guildId)!;
lastTalked.set(message.author.id, new Date());
+ if (!HighlightManager.keep.has(message.author.id)) HighlightManager.keep.add(message.author.id);
}
}
diff --git a/src/lib/common/util/Moderation.ts b/lib/common/Moderation.ts
index 60e32c0..60e32c0 100644
--- a/src/lib/common/util/Moderation.ts
+++ b/lib/common/Moderation.ts
diff --git a/src/lib/common/Sentry.ts b/lib/common/Sentry.ts
index 2792203..446ec27 100644
--- a/src/lib/common/Sentry.ts
+++ b/lib/common/Sentry.ts
@@ -1,7 +1,7 @@
import { RewriteFrames } from '@sentry/integrations';
import * as SentryNode from '@sentry/node';
import { Integrations } from '@sentry/node';
-import type { Config } from '../../../config/Config.js';
+import type { Config } from '../../config/Config.js';
export class Sentry {
public constructor(rootdir: string, config: Config) {
diff --git a/src/lib/common/tags.ts b/lib/common/tags.ts
index 098cf29..098cf29 100644
--- a/src/lib/common/tags.ts
+++ b/lib/common/tags.ts
diff --git a/src/lib/extensions/discord-akairo/BushArgumentTypeCaster.ts b/lib/extensions/discord-akairo/BushArgumentTypeCaster.ts
index def7ad6..def7ad6 100644
--- a/src/lib/extensions/discord-akairo/BushArgumentTypeCaster.ts
+++ b/lib/extensions/discord-akairo/BushArgumentTypeCaster.ts
diff --git a/src/lib/extensions/discord-akairo/BushClient.ts b/lib/extensions/discord-akairo/BushClient.ts
index 9ca02a2..1a6bb8c 100644
--- a/src/lib/extensions/discord-akairo/BushClient.ts
+++ b/lib/extensions/discord-akairo/BushClient.ts
@@ -46,10 +46,11 @@ import path from 'path';
import readline from 'readline';
import type { Options as SequelizeOptions, Sequelize as SequelizeType } from 'sequelize';
import { fileURLToPath } from 'url';
-import type { Config } from '../../../../config/Config.js';
-import { tinyColor } from '../../../arguments/tinyColor.js';
-import UpdateCacheTask from '../../../tasks/cache/updateCache.js';
-import UpdateStatsTask from '../../../tasks/feature/updateStats.js';
+import type { Config } from '../../../config/Config.js';
+import UpdateCacheTask from '../../../src/tasks/cache/updateCache.js';
+import UpdateStatsTask from '../../../src/tasks/feature/updateStats.js';
+import { tinyColor } from '../../arguments/tinyColor.js';
+import { BushCache } from '../../common/BushCache.js';
import { HighlightManager } from '../../common/HighlightManager.js';
import { ActivePunishment } from '../../models/instance/ActivePunishment.js';
import { Guild as GuildDB } from '../../models/instance/Guild.js';
@@ -64,7 +65,6 @@ import { MemberCount } from '../../models/shared/MemberCount.js';
import { Shared } from '../../models/shared/Shared.js';
import { Stat } from '../../models/shared/Stat.js';
import { AllowedMentions } from '../../utils/AllowedMentions.js';
-import { BushCache } from '../../utils/BushCache.js';
import { BushClientUtils } from '../../utils/BushClientUtils.js';
import { BushLogger } from '../../utils/BushLogger.js';
import { ExtendedGuild } from '../discord.js/ExtendedGuild.js';
@@ -251,7 +251,16 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re
status: 'online'
},
allowedMentions: AllowedMentions.none(), // no mentions by default
- makeCache: Options.cacheWithLimits({}),
+ makeCache: Options.cacheWithLimits({
+ PresenceManager: {
+ maxSize: 0,
+ keepOverLimit: (_, key) => {
+ if (config.owners.includes(key)) return true;
+
+ return HighlightManager.keep.has(key);
+ }
+ }
+ }),
failIfNotExists: false,
rest: { api: 'https://canary.discord.com/api' }
});
@@ -261,15 +270,18 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re
/* =-=-= handlers =-=-= */
this.listenerHandler = new BushListenerHandler(this, {
- directory: path.join(__dirname, '..', '..', '..', 'listeners'),
+ directory: path.join(__dirname, '..', '..', '..', 'src', 'listeners'),
+ extensions: ['.js'],
automateCategories: true
});
this.inhibitorHandler = new BushInhibitorHandler(this, {
- directory: path.join(__dirname, '..', '..', '..', 'inhibitors'),
+ directory: path.join(__dirname, '..', '..', '..', 'src', 'inhibitors'),
+ extensions: ['.js'],
automateCategories: true
});
this.taskHandler = new BushTaskHandler(this, {
- directory: path.join(__dirname, '..', '..', '..', 'tasks'),
+ directory: path.join(__dirname, '..', '..', '..', 'src', 'tasks'),
+ extensions: ['.js'],
automateCategories: true
});
@@ -299,7 +311,8 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re
};
this.commandHandler = new BushCommandHandler(this, {
- directory: path.join(__dirname, '..', '..', '..', 'commands'),
+ directory: path.join(__dirname, '..', '..', '..', 'src', 'commands'),
+ extensions: ['.js'],
prefix: async ({ guild }: Message) => {
if (this.config.isDevelopment) return 'dev ';
if (!guild) return this.config.prefix;
@@ -330,7 +343,8 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re
aliasReplacement: /-/g
});
this.contextMenuCommandHandler = new ContextMenuCommandHandler(this, {
- directory: path.join(__dirname, '..', '..', '..', 'context-menu-commands'),
+ directory: path.join(__dirname, '..', '..', '..', 'src', 'context-menu-commands'),
+ extensions: ['.js'],
automateCategories: true
});
diff --git a/src/lib/extensions/discord-akairo/BushCommand.ts b/lib/extensions/discord-akairo/BushCommand.ts
index dc2295f..dc2295f 100644
--- a/src/lib/extensions/discord-akairo/BushCommand.ts
+++ b/lib/extensions/discord-akairo/BushCommand.ts
diff --git a/src/lib/extensions/discord-akairo/BushCommandHandler.ts b/lib/extensions/discord-akairo/BushCommandHandler.ts
index da49af9..da49af9 100644
--- a/src/lib/extensions/discord-akairo/BushCommandHandler.ts
+++ b/lib/extensions/discord-akairo/BushCommandHandler.ts
diff --git a/src/lib/extensions/discord-akairo/BushInhibitor.ts b/lib/extensions/discord-akairo/BushInhibitor.ts
index be396cf..be396cf 100644
--- a/src/lib/extensions/discord-akairo/BushInhibitor.ts
+++ b/lib/extensions/discord-akairo/BushInhibitor.ts
diff --git a/src/lib/extensions/discord-akairo/BushInhibitorHandler.ts b/lib/extensions/discord-akairo/BushInhibitorHandler.ts
index 5e4fb6c..5e4fb6c 100644
--- a/src/lib/extensions/discord-akairo/BushInhibitorHandler.ts
+++ b/lib/extensions/discord-akairo/BushInhibitorHandler.ts
diff --git a/src/lib/extensions/discord-akairo/BushListener.ts b/lib/extensions/discord-akairo/BushListener.ts
index 6917641..6917641 100644
--- a/src/lib/extensions/discord-akairo/BushListener.ts
+++ b/lib/extensions/discord-akairo/BushListener.ts
diff --git a/src/lib/extensions/discord-akairo/BushListenerHandler.ts b/lib/extensions/discord-akairo/BushListenerHandler.ts
index 9c3e4af..9c3e4af 100644
--- a/src/lib/extensions/discord-akairo/BushListenerHandler.ts
+++ b/lib/extensions/discord-akairo/BushListenerHandler.ts
diff --git a/src/lib/extensions/discord-akairo/BushTask.ts b/lib/extensions/discord-akairo/BushTask.ts
index 1b70c88..1b70c88 100644
--- a/src/lib/extensions/discord-akairo/BushTask.ts
+++ b/lib/extensions/discord-akairo/BushTask.ts
diff --git a/src/lib/extensions/discord-akairo/BushTaskHandler.ts b/lib/extensions/discord-akairo/BushTaskHandler.ts
index 6535abb..6535abb 100644
--- a/src/lib/extensions/discord-akairo/BushTaskHandler.ts
+++ b/lib/extensions/discord-akairo/BushTaskHandler.ts
diff --git a/src/lib/extensions/discord-akairo/SlashMessage.ts b/lib/extensions/discord-akairo/SlashMessage.ts
index 0a6669b..0a6669b 100644
--- a/src/lib/extensions/discord-akairo/SlashMessage.ts
+++ b/lib/extensions/discord-akairo/SlashMessage.ts
diff --git a/src/lib/extensions/discord.js/BushClientEvents.ts b/lib/extensions/discord.js/BushClientEvents.ts
index 22bae65..22bae65 100644
--- a/src/lib/extensions/discord.js/BushClientEvents.ts
+++ b/lib/extensions/discord.js/BushClientEvents.ts
diff --git a/src/lib/extensions/discord.js/ExtendedGuild.ts b/lib/extensions/discord.js/ExtendedGuild.ts
index 3dce7ca..63ee2fd 100644
--- a/src/lib/extensions/discord.js/ExtendedGuild.ts
+++ b/lib/extensions/discord.js/ExtendedGuild.ts
@@ -39,7 +39,7 @@ import {
type WebhookMessageOptions
} from 'discord.js';
import _ from 'lodash';
-import * as Moderation from '../../common/util/Moderation.js';
+import * as Moderation from '../../common/Moderation.js';
import { Guild as GuildDB } from '../../models/instance/Guild.js';
import { ModLogType } from '../../models/instance/ModLog.js';
import { addOrRemoveFromArray } from '../../utils/BushUtils.js';
@@ -237,7 +237,10 @@ export class ExtendedGuild extends Guild {
message: string | MessagePayload | MessageOptions
): Promise<Message | null | undefined> {
const logChannel = await this.getLogChannel(logType);
- if (!logChannel || !logChannel.isTextBased()) return;
+ if (!logChannel || !logChannel.isTextBased()) {
+ void this.client.console.warn('sendLogChannel', `No log channel found for <<${logType}<< in <<${this.name}>>.`);
+ return;
+ }
if (
!logChannel
.permissionsFor(this.members.me!.id)
diff --git a/src/lib/extensions/discord.js/ExtendedGuildMember.ts b/lib/extensions/discord.js/ExtendedGuildMember.ts
index f8add83..f8add83 100644
--- a/src/lib/extensions/discord.js/ExtendedGuildMember.ts
+++ b/lib/extensions/discord.js/ExtendedGuildMember.ts
diff --git a/src/lib/extensions/discord.js/ExtendedMessage.ts b/lib/extensions/discord.js/ExtendedMessage.ts
index 4748803..1bb0904 100644
--- a/src/lib/extensions/discord.js/ExtendedMessage.ts
+++ b/lib/extensions/discord.js/ExtendedMessage.ts
@@ -1,6 +1,6 @@
import { CommandUtil } from 'discord-akairo';
import { Message, type Client } from 'discord.js';
-import { type RawMessageData } from 'discord.js/typings/rawDataTypes.js';
+import type { RawMessageData } from 'discord.js/typings/rawDataTypes.js';
export class ExtendedMessage<Cached extends boolean = boolean> extends Message<Cached> {
public declare util: CommandUtil<Message>;
diff --git a/src/lib/extensions/discord.js/ExtendedUser.ts b/lib/extensions/discord.js/ExtendedUser.ts
index 23de523..23de523 100644
--- a/src/lib/extensions/discord.js/ExtendedUser.ts
+++ b/lib/extensions/discord.js/ExtendedUser.ts
diff --git a/src/lib/extensions/global.ts b/lib/extensions/global.ts
index a9020d7..a9020d7 100644
--- a/src/lib/extensions/global.ts
+++ b/lib/extensions/global.ts
diff --git a/src/lib/index.ts b/lib/index.ts
index 3e57f9e..5a8ecde 100644
--- a/src/lib/index.ts
+++ b/lib/index.ts
@@ -1,12 +1,13 @@
-export * from './common/AutoMod.js';
+export * from './automod/AutomodShared.js';
+export * from './automod/MemberAutomod.js';
+export * from './automod/MessageAutomod.js';
+export * from './automod/PresenceAutomod.js';
+export * from './common/BushCache.js';
export * from './common/ButtonPaginator.js';
+export * from './common/CanvasProgressBar.js';
export * from './common/ConfirmationPrompt.js';
export * from './common/DeleteButton.js';
-export type { BushInspectOptions } from './common/typings/BushInspectOptions.js';
-export type { CodeBlockLang } from './common/typings/CodeBlockLang.js';
-export * as Arg from './common/util/Arg.js';
-export * as Format from './common/util/Format.js';
-export * as Moderation from './common/util/Moderation.js';
+export * as Moderation from './common/Moderation.js';
export type {
AppealButtonId,
CreateModLogEntryOptions,
@@ -16,7 +17,7 @@ export type {
PunishmentTypePresent,
RemovePunishmentEntryOptions,
SimpleCreateModLogEntryOptions
-} from './common/util/Moderation.js';
+} from './common/Moderation.js';
export * from './extensions/discord-akairo/BushArgumentTypeCaster.js';
export * from './extensions/discord-akairo/BushClient.js';
export * from './extensions/discord-akairo/BushCommand.js';
@@ -45,9 +46,11 @@ export * from './models/shared/Global.js';
export * from './models/shared/MemberCount.js';
export * from './models/shared/Shared.js';
export * from './models/shared/Stat.js';
+export type { BushInspectOptions } from './types/BushInspectOptions.js';
+export type { CodeBlockLang } from './types/CodeBlockLang.js';
export * from './utils/AllowedMentions.js';
-export * from './utils/BushCache.js';
+export * as Arg from './utils/Arg.js';
export * from './utils/BushConstants.js';
export * from './utils/BushLogger.js';
export * from './utils/BushUtils.js';
-export * from './utils/CanvasProgressBar.js';
+export * as Format from './utils/Format.js';
diff --git a/src/lib/models/BaseModel.ts b/lib/models/BaseModel.ts
index e503317..8fba5e5 100644
--- a/src/lib/models/BaseModel.ts
+++ b/lib/models/BaseModel.ts
@@ -1,4 +1,4 @@
-const { Model } = (await import('sequelize')).default;
+import { Model } from 'sequelize';
export abstract class BaseModel<A, B> extends Model<A, B> {
/**
diff --git a/src/lib/models/instance/ActivePunishment.ts b/lib/models/instance/ActivePunishment.ts
index 38012ca..38012ca 100644
--- a/src/lib/models/instance/ActivePunishment.ts
+++ b/lib/models/instance/ActivePunishment.ts
diff --git a/src/lib/models/instance/Guild.ts b/lib/models/instance/Guild.ts
index f0cac74..f258d48 100644
--- a/src/lib/models/instance/Guild.ts
+++ b/lib/models/instance/Guild.ts
@@ -1,6 +1,6 @@
import { ChannelType, Constants, type Snowflake } from 'discord.js';
import { type Sequelize } from 'sequelize';
-import { BadWordDetails } from '../../common/AutoMod.js';
+import { BadWordDetails } from '../../automod/AutomodShared.js';
import { type BushClient } from '../../extensions/discord-akairo/BushClient.js';
import { BaseModel } from '../BaseModel.js';
const { DataTypes } = (await import('sequelize')).default;
@@ -199,7 +199,7 @@ const asGuildSetting = <T>(et: { [K in keyof T]: PartialBy<GuildSetting, 'config
return et as { [K in keyof T]: GuildSetting };
};
-const { default: config } = await import('../../../../config/options.js');
+const { default: config } = await import('../../../config/options.js');
export const guildSettingsObj = asGuildSetting({
prefix: {
@@ -326,6 +326,15 @@ export const guildFeaturesObj = asGuildFeature({
name: 'Delete Scam Mentions',
description: 'Deletes messages with @everyone and @here mentions that have common scam phrases.'
},
+ automodPresence: {
+ name: 'Automod Presence',
+ description: 'Logs presence changes that trigger automod.',
+ hidden: true
+ },
+ automodMembers: {
+ name: 'Automod Members',
+ description: "Logs members' usernames and nicknames changes if they match automod."
+ },
blacklistedFile: {
name: 'Blacklisted File',
description: 'Automatically deletes malicious files.'
diff --git a/src/lib/models/instance/Highlight.ts b/lib/models/instance/Highlight.ts
index 5889fad..5889fad 100644
--- a/src/lib/models/instance/Highlight.ts
+++ b/lib/models/instance/Highlight.ts
diff --git a/src/lib/models/instance/Level.ts b/lib/models/instance/Level.ts
index d8d16f0..d8d16f0 100644
--- a/src/lib/models/instance/Level.ts
+++ b/lib/models/instance/Level.ts
diff --git a/src/lib/models/instance/ModLog.ts b/lib/models/instance/ModLog.ts
index c25f043..c25f043 100644
--- a/src/lib/models/instance/ModLog.ts
+++ b/lib/models/instance/ModLog.ts
diff --git a/src/lib/models/instance/Reminder.ts b/lib/models/instance/Reminder.ts
index 964ea63..964ea63 100644
--- a/src/lib/models/instance/Reminder.ts
+++ b/lib/models/instance/Reminder.ts
diff --git a/src/lib/models/instance/StickyRole.ts b/lib/models/instance/StickyRole.ts
index 00e98ce..00e98ce 100644
--- a/src/lib/models/instance/StickyRole.ts
+++ b/lib/models/instance/StickyRole.ts
diff --git a/src/lib/models/shared/Global.ts b/lib/models/shared/Global.ts
index b1aa0cc..b1aa0cc 100644
--- a/src/lib/models/shared/Global.ts
+++ b/lib/models/shared/Global.ts
diff --git a/src/lib/models/shared/GuildCount.ts b/lib/models/shared/GuildCount.ts
index 51e571a..7afef56 100644
--- a/src/lib/models/shared/GuildCount.ts
+++ b/lib/models/shared/GuildCount.ts
@@ -1,6 +1,5 @@
-import { type Sequelize } from 'sequelize';
-import { Environment } from '../../../../config/Config.js';
-const { DataTypes, Model } = (await import('sequelize')).default;
+import { DataTypes, Model, type Sequelize } from 'sequelize';
+import { Environment } from '../../../config/Config.js';
export interface GuildCountModel {
timestamp: Date;
diff --git a/src/lib/models/shared/MemberCount.ts b/lib/models/shared/MemberCount.ts
index ea8795c..200a58e 100644
--- a/src/lib/models/shared/MemberCount.ts
+++ b/lib/models/shared/MemberCount.ts
@@ -1,5 +1,4 @@
-import { type Sequelize } from 'sequelize';
-const { DataTypes, Model } = (await import('sequelize')).default;
+import { DataTypes, Model, type Sequelize } from 'sequelize';
export interface MemberCountModel {
timestamp: Date;
diff --git a/src/lib/models/shared/Shared.ts b/lib/models/shared/Shared.ts
index 4d13011..dec77d1 100644
--- a/src/lib/models/shared/Shared.ts
+++ b/lib/models/shared/Shared.ts
@@ -1,6 +1,6 @@
import { Snowflake } from 'discord.js';
import type { Sequelize } from 'sequelize';
-import type { BadWords } from '../../common/AutoMod.js';
+import { BadWords } from '../../automod/AutomodShared.js';
import { BaseModel } from '../BaseModel.js';
const { DataTypes } = (await import('sequelize')).default;
diff --git a/src/lib/models/shared/Stat.ts b/lib/models/shared/Stat.ts
index 8e2e0b3..8e2e0b3 100644
--- a/src/lib/models/shared/Stat.ts
+++ b/lib/models/shared/Stat.ts
diff --git a/lib/tsconfig.json b/lib/tsconfig.json
new file mode 100644
index 0000000..e6d554e
--- /dev/null
+++ b/lib/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "../../dist/lib",
+ "composite": true
+ },
+ "include": ["lib/**/*.ts"],
+ "references": [{ "path": "../config" }]
+}
diff --git a/src/lib/common/typings/BushInspectOptions.ts b/lib/types/BushInspectOptions.ts
index 30ed01a..30ed01a 100644
--- a/src/lib/common/typings/BushInspectOptions.ts
+++ b/lib/types/BushInspectOptions.ts
diff --git a/src/lib/common/typings/CodeBlockLang.ts b/lib/types/CodeBlockLang.ts
index d0eb4f3..d0eb4f3 100644
--- a/src/lib/common/typings/CodeBlockLang.ts
+++ b/lib/types/CodeBlockLang.ts
diff --git a/src/lib/utils/AllowedMentions.ts b/lib/utils/AllowedMentions.ts
index d2eb030..d2eb030 100644
--- a/src/lib/utils/AllowedMentions.ts
+++ b/lib/utils/AllowedMentions.ts
diff --git a/src/lib/common/util/Arg.ts b/lib/utils/Arg.ts
index d362225..d362225 100644
--- a/src/lib/common/util/Arg.ts
+++ b/lib/utils/Arg.ts
diff --git a/src/lib/utils/BushClientUtils.ts b/lib/utils/BushClientUtils.ts
index 920ff40..68a1dc3 100644
--- a/src/lib/utils/BushClientUtils.ts
+++ b/lib/utils/BushClientUtils.ts
@@ -17,15 +17,15 @@ import {
} from 'discord.js';
import got from 'got';
import _ from 'lodash';
-import { ConfigChannelKey } from '../../../config/Config.js';
-import CommandErrorListener from '../../listeners/commands/commandError.js';
-import { BushInspectOptions } from '../common/typings/BushInspectOptions.js';
-import { CodeBlockLang } from '../common/typings/CodeBlockLang.js';
+import { ConfigChannelKey } from '../../config/Config.js';
+import CommandErrorListener from '../../src/listeners/commands/commandError.js';
+import { GlobalCache, SharedCache } from '../common/BushCache.js';
import { CommandMessage } from '../extensions/discord-akairo/BushCommand.js';
import { SlashMessage } from '../extensions/discord-akairo/SlashMessage.js';
import { Global } from '../models/shared/Global.js';
import { Shared } from '../models/shared/Shared.js';
-import { GlobalCache, SharedCache } from './BushCache.js';
+import { BushInspectOptions } from '../types/BushInspectOptions.js';
+import { CodeBlockLang } from '../types/CodeBlockLang.js';
import { emojis, Pronoun, PronounCode, pronounMapping, regex } from './BushConstants.js';
import { addOrRemoveFromArray, formatError, inspect } from './BushUtils.js';
@@ -398,9 +398,10 @@ export class BushClientUtils {
},
followRedirect: true
})
- .json()) as { data: { link: string } };
+ .json()
+ .catch(() => null)) as { data: { link: string } | undefined };
- return resp.data.link;
+ return resp.data?.link ?? null;
}
/**
diff --git a/src/lib/utils/BushConstants.ts b/lib/utils/BushConstants.ts
index 090616c..090616c 100644
--- a/src/lib/utils/BushConstants.ts
+++ b/lib/utils/BushConstants.ts
diff --git a/src/lib/utils/BushLogger.ts b/lib/utils/BushLogger.ts
index 4acda69..4acda69 100644
--- a/src/lib/utils/BushLogger.ts
+++ b/lib/utils/BushLogger.ts
diff --git a/src/lib/utils/BushUtils.ts b/lib/utils/BushUtils.ts
index 75aded3..34ea461 100644
--- a/src/lib/utils/BushUtils.ts
+++ b/lib/utils/BushUtils.ts
@@ -30,7 +30,7 @@ import {
import got from 'got';
import { DeepWritable } from 'ts-essentials';
import { inspect as inspectUtil, promisify } from 'util';
-import * as Format from '../common/util/Format.js';
+import * as Format from './Format.js';
export type StripPrivate<T> = { [K in keyof T]: T[K] extends Record<string, any> ? StripPrivate<T[K]> : T[K] };
export type ValueOf<T> = T[keyof T];
@@ -87,6 +87,7 @@ export function chunk<T>(arr: T[], perChunk: number): T[][] {
*/
export async function mcUUID(username: string, dashed = false): Promise<string> {
const apiRes = (await got.get(`https://api.ashcon.app/mojang/v2/user/${username}`).json()) as UuidRes;
+
return dashed ? apiRes.uuid : apiRes.uuid.replace(/-/g, '');
}
diff --git a/src/lib/common/util/Format.ts b/lib/utils/Format.ts
index debaf4b..debaf4b 100644
--- a/src/lib/common/util/Format.ts
+++ b/lib/utils/Format.ts
diff --git a/src/lib/common/util/Minecraft.ts b/lib/utils/Minecraft.ts
index a12ebf2..bb5fbfe 100644
--- a/src/lib/common/util/Minecraft.ts
+++ b/lib/utils/Minecraft.ts
@@ -1,3 +1,5 @@
+/* eslint-disable */
+
import { Byte, Int, parse } from '@ironm00n/nbt-ts';
import { BitField } from 'discord.js';
import path from 'path';
@@ -222,7 +224,7 @@ export function removeMCFormatting(str: string) {
return str.replaceAll(formattingCode, '');
}
-const repo = path.join(__dirname, '..', '..', '..', '..', '..', 'neu-item-repo-dangerous');
+const repo = path.join(__dirname, '..', '..', '..', 'neu-item-repo-dangerous');
export interface NbtTag {
overrideMeta?: Byte;
diff --git a/src/lib/common/util/Minecraft_Test.ts b/lib/utils/Minecraft_Test.ts
index 26ca648..26ca648 100644
--- a/src/lib/common/util/Minecraft_Test.ts
+++ b/lib/utils/Minecraft_Test.ts
diff --git a/test.js b/misc/test.js
index d0840ec..d0840ec 100644
--- a/test.js
+++ b/misc/test.js
diff --git a/test.png b/misc/test.png
index 942a85e..942a85e 100644
--- a/test.png
+++ b/misc/test.png
Binary files differ
diff --git a/tooltips.nnb b/misc/tooltips.nnb
index 6e36999..6e36999 100644
--- a/tooltips.nnb
+++ b/misc/tooltips.nnb
diff --git a/package.json b/package.json
index 5b25d9d..2f73278 100644
--- a/package.json
+++ b/package.json
@@ -24,8 +24,8 @@
"build:esbuild": "yarn rimraf dist && yarn esbuild --sourcemap=inline --outdir=dist --platform=node --target=es2020 --format=esm --log-level=warning src/**/*.ts",
"build:tsc": "yarn rimraf dist && yarn tsc",
"build:tsc:no-emit": "yarn tsc --noEmit",
- "build:keep": "yarn tsc ",
- "start:raw": "node --enable-source-maps --experimental-json-modules --no-warnings dist/src/bot.js",
+ "build:keep": "yarn tsc",
+ "start:raw": "node --enable-source-maps --experimental-json-modules --no-warnings --pending-deprecation dist/src/bot.js",
"start:esbuild": "yarn build:esbuild && yarn start:raw",
"start": "yarn build:tsc && yarn start:raw",
"start:keep": "yarn build:keep && yarn start:raw",
@@ -33,7 +33,7 @@
"dev": "yarn build:tsc && yarn start:raw",
"test": "yarn lint && yarn tsc --noEmit",
"format": "yarn prettier . --write",
- "lint": "yarn eslint --ext js,jsx,ts,tsx src",
+ "lint": "yarn eslint yarn eslint src lib config tests",
"format:check": "yarn prettier . --check",
"upgrade": "yarn rimraf yarn.lock && yarn cache clean && yarn install && yarn up && yarn up -R && yarn set version latest && git submodule update --recursive --remote",
"upgrade:sdk": "yarn dlx @yarnpkg/sdks vscode",
@@ -44,19 +44,19 @@
},
"imports": {
"#lib": {
- "default": "./src/lib/index.js"
+ "default": "./lib/index.js"
},
"#constants": {
- "default": "./src/lib/utils/BushConstants.js"
+ "default": "./lib/utils/BushConstants.js"
},
"#args": {
- "default": "./src/arguments/index.js"
+ "default": "./lib/arguments/index.js"
},
"#commands": {
"default": "./src/commands/index.js"
},
"#tags": {
- "default": "./src/lib/common/tags.js"
+ "default": "./lib/common/tags.js"
}
},
"dependencies": {
diff --git a/src/bot.ts b/src/bot.ts
index 038fbbb..10818e9 100644
--- a/src/bot.ts
+++ b/src/bot.ts
@@ -1,4 +1,4 @@
-import { init } from './lib/utils/BushLogger.js';
+import { init } from '../lib/utils/BushLogger.js';
// creates proxies on console.log and console.warn
// also starts a REPL session
init();
@@ -6,8 +6,8 @@ init();
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import { default as config } from '../config/options.js';
-import { Sentry } from './lib/common/Sentry.js';
-import { BushClient } from './lib/index.js';
+import { Sentry } from '../lib/common/Sentry.js';
+import { BushClient } from '../lib/extensions/discord-akairo/BushClient.js';
const isDry = process.argv.includes('dry');
if (!isDry && config.credentials.sentryDsn !== null) new Sentry(dirname(fileURLToPath(import.meta.url)) || process.cwd(), config);
diff --git a/src/commands/admin/channelPermissions.ts b/src/commands/admin/channelPermissions.ts
index 0b09e54..21abd04 100644
--- a/src/commands/admin/channelPermissions.ts
+++ b/src/commands/admin/channelPermissions.ts
@@ -1,6 +1,5 @@
import {
Arg,
- BushCommand,
ButtonPaginator,
clientSendAndPermCheck,
emojis,
@@ -11,6 +10,7 @@ import {
} from '#lib';
import assert from 'assert/strict';
import { ApplicationCommandOptionType, EmbedBuilder, PermissionFlagsBits } from 'discord.js';
+import { BushCommand } from '../../../lib/extensions/discord-akairo/BushCommand.js';
export default class ChannelPermissionsCommand extends BushCommand {
public constructor() {
diff --git a/src/commands/dev/test.ts b/src/commands/dev/test.ts
index ac0ad83..0606497 100644
--- a/src/commands/dev/test.ts
+++ b/src/commands/dev/test.ts
@@ -18,9 +18,9 @@ import {
type ApplicationCommand,
type Collection
} from 'discord.js';
-import badLinksSecretArray from '../../lib/badlinks-secret.js';
-import badLinksArray from '../../lib/badlinks.js';
-import badWords from '../../lib/badwords.js';
+import badLinksSecretArray from '../../../lib/badlinks-secret.js';
+import badLinksArray from '../../../lib/badlinks.js';
+import badWords from '../../../lib/badwords.js';
export default class TestCommand extends BushCommand {
public constructor() {
diff --git a/src/commands/info/help.ts b/src/commands/info/help.ts
index 348c74f..62f177e 100644
--- a/src/commands/info/help.ts
+++ b/src/commands/info/help.ts
@@ -9,6 +9,7 @@ import {
type OptArgType,
type SlashMessage
} from '#lib';
+import { stripIndent } from '#tags';
import assert from 'assert/strict';
import {
ActionRowBuilder,
@@ -21,7 +22,6 @@ import {
} from 'discord.js';
import Fuse from 'fuse.js';
import packageDotJSON from '../../../package.json' assert { type: 'json' };
-import { stripIndent } from '../../lib/common/tags.js';
assert(Fuse);
assert(packageDotJSON);
diff --git a/src/commands/moderation/massEvidence.ts b/src/commands/moderation/massEvidence.ts
index 62f4825..cecf273 100644
--- a/src/commands/moderation/massEvidence.ts
+++ b/src/commands/moderation/massEvidence.ts
@@ -13,7 +13,7 @@ import {
} from '#lib';
import assert from 'assert/strict';
import { ApplicationCommandOptionType, PermissionFlagsBits } from 'discord.js';
-import { EvidenceCommand } from '../index.js';
+import EvidenceCommand from './evidence.js';
export default class MassEvidenceCommand extends BushCommand {
public constructor() {
diff --git a/src/commands/moderation/myLogs.ts b/src/commands/moderation/myLogs.ts
index ab67a18..c1cc448 100644
--- a/src/commands/moderation/myLogs.ts
+++ b/src/commands/moderation/myLogs.ts
@@ -12,7 +12,7 @@ import {
} from '#lib';
import { ApplicationCommandOptionType } from 'discord.js';
-import { input, sanitizeInputForDiscord } from '../../lib/common/util/Format.js';
+import { input, sanitizeInputForDiscord } from '../../../lib/utils/Format.js';
import ModlogCommand from './modlog.js';
export default class MyLogsCommand extends BushCommand {
public constructor() {
diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts
index 648a178..620f499 100644
--- a/src/commands/moderation/unmute.ts
+++ b/src/commands/moderation/unmute.ts
@@ -14,7 +14,7 @@ import {
} from '#lib';
import assert from 'assert/strict';
import { ApplicationCommandOptionType, PermissionFlagsBits, type GuildMember } from 'discord.js';
-import { BushCommand } from '../../lib/extensions/discord-akairo/BushCommand.js';
+import { BushCommand } from '../../../lib/extensions/discord-akairo/BushCommand.js';
export default class UnmuteCommand extends BushCommand {
public constructor() {
diff --git a/src/commands/moulberry-bush/neuRepo.ts b/src/commands/moulberry-bush/neuRepo.ts
index d07ba53..fcb6f23 100644
--- a/src/commands/moulberry-bush/neuRepo.ts
+++ b/src/commands/moulberry-bush/neuRepo.ts
@@ -10,7 +10,7 @@ import {
import { dirname, join } from 'path';
import tinycolor from 'tinycolor2';
import { fileURLToPath } from 'url';
-import { formattingInfo, RawNeuItem } from '../../lib/common/util/Minecraft.js';
+import { formattingInfo, RawNeuItem } from '../../../lib/utils/Minecraft.js';
export default class NeuRepoCommand extends BushCommand {
public static items: { name: string; id: string }[] = [];
diff --git a/src/commands/moulberry-bush/rule.ts b/src/commands/moulberry-bush/rule.ts
index 25a3ef0..ab5500d 100644
--- a/src/commands/moulberry-bush/rule.ts
+++ b/src/commands/moulberry-bush/rule.ts
@@ -8,8 +8,8 @@ import {
type OptArgType,
type SlashMessage
} from '#lib';
+import { stripIndent } from '#tags';
import { ApplicationCommandOptionType, EmbedBuilder, PermissionFlagsBits } from 'discord.js';
-import { stripIndent } from '../../lib/common/tags.js';
const rules = [
{
diff --git a/src/commands/utilities/calculator.ts b/src/commands/utilities/calculator.ts
index c9dbbf2..dc5593b 100644
--- a/src/commands/utilities/calculator.ts
+++ b/src/commands/utilities/calculator.ts
@@ -52,7 +52,7 @@ export default class CalculatorCommand extends BushCommand {
name: '📤 Output',
value: await this.client.utils.inspectCleanRedactCodeblock(calculated.toString(), 'mma')
});
- } catch (error) {
+ } catch (error: any) {
decodedEmbed
.setTitle(`${emojis.errorFull} Unable to Calculate Expression`)
.setColor(colors.error)
diff --git a/src/commands/utilities/highlight-block.ts b/src/commands/utilities/highlight-block.ts
index a450d71..58e7766 100644
--- a/src/commands/utilities/highlight-block.ts
+++ b/src/commands/utilities/highlight-block.ts
@@ -2,7 +2,7 @@ import { AllowedMentions, BushCommand, emojis, type ArgType, type CommandMessage
import assert from 'assert/strict';
import { Argument, ArgumentGeneratorReturn } from 'discord-akairo';
import { BaseChannel, GuildMember, User } from 'discord.js';
-import { HighlightBlockResult } from '../../lib/common/HighlightManager.js';
+import { HighlightBlockResult } from '../../../lib/common/HighlightManager.js';
import { highlightSubcommands } from './highlight-!.js';
export default class HighlightBlockCommand extends BushCommand {
diff --git a/src/commands/utilities/highlight-unblock.ts b/src/commands/utilities/highlight-unblock.ts
index 702fa65..2238831 100644
--- a/src/commands/utilities/highlight-unblock.ts
+++ b/src/commands/utilities/highlight-unblock.ts
@@ -2,7 +2,7 @@ import { AllowedMentions, BushCommand, emojis, type ArgType, type CommandMessage
import assert from 'assert';
import { Argument, ArgumentGeneratorReturn } from 'discord-akairo';
import { BaseChannel, GuildMember, User } from 'discord.js';
-import { HighlightUnblockResult } from '../../lib/common/HighlightManager.js';
+import { HighlightUnblockResult } from '../../../lib/common/HighlightManager.js';
import { highlightSubcommands } from './highlight-!.js';
export default class HighlightUnblockCommand extends BushCommand {
diff --git a/src/commands/utilities/uuid.ts b/src/commands/utilities/uuid.ts
index 3f99e66..04d4013 100644
--- a/src/commands/utilities/uuid.ts
+++ b/src/commands/utilities/uuid.ts
@@ -23,7 +23,7 @@ export default class UuidCommand extends BushCommand {
{
id: 'ign',
description: 'The ign to find the ign of.',
- customType: /\w{1,16}/im,
+ customType: /^\w{1,16}$/im,
readableType: 'string[1,16]',
prompt: 'What ign would you like to find the uuid of?',
retry: '{error} Choose a valid ign.',
diff --git a/src/commands/utilities/wolframAlpha.ts b/src/commands/utilities/wolframAlpha.ts
index b35e14f..5ba55f7 100644
--- a/src/commands/utilities/wolframAlpha.ts
+++ b/src/commands/utilities/wolframAlpha.ts
@@ -87,6 +87,8 @@ export default class WolframAlphaCommand extends BushCommand {
});
}
} catch (error) {
+ assert(error instanceof Error);
+
decodedEmbed
.setTitle(`${emojis.errorFull} Unable to Query Expression`)
.setColor(colors.error)
diff --git a/src/context-menu-commands/message/viewRaw.ts b/src/context-menu-commands/message/viewRaw.ts
index 6cfe552..f216a59 100644
--- a/src/context-menu-commands/message/viewRaw.ts
+++ b/src/context-menu-commands/message/viewRaw.ts
@@ -1,6 +1,6 @@
-import { ViewRawCommand } from '#commands';
import { ContextMenuCommand } from 'discord-akairo';
import { ApplicationCommandType, type ContextMenuCommandInteraction, type Message } from 'discord.js';
+import ViewRawCommand from '../../commands/utilities/viewRaw.js';
export default class ViewRawContextMenuCommand extends ContextMenuCommand {
public constructor() {
diff --git a/src/context-menu-commands/user/modlog.ts b/src/context-menu-commands/user/modlog.ts
index c78396e..91b1b62 100644
--- a/src/context-menu-commands/user/modlog.ts
+++ b/src/context-menu-commands/user/modlog.ts
@@ -1,7 +1,7 @@
-import { ModlogCommand } from '#commands';
import { emojis, SlashMessage } from '#lib';
import { CommandUtil, ContextMenuCommand } from 'discord-akairo';
import { ApplicationCommandType, type ContextMenuCommandInteraction } from 'discord.js';
+import ModlogCommand from '../../commands/moderation/modlog.js';
export default class ModlogContextMenuCommand extends ContextMenuCommand {
public constructor() {
diff --git a/src/context-menu-commands/user/userInfo.ts b/src/context-menu-commands/user/userInfo.ts
index 6d7f3b6..0d19cce 100644
--- a/src/context-menu-commands/user/userInfo.ts
+++ b/src/context-menu-commands/user/userInfo.ts
@@ -1,7 +1,7 @@
-import { UserInfoCommand } from '#commands';
import { format } from '#lib';
import { ContextMenuCommand } from 'discord-akairo';
import { ApplicationCommandType, type ContextMenuCommandInteraction, type Guild } from 'discord.js';
+import UserInfoCommand from '../../commands/info/userInfo.js';
export default class UserInfoContextMenuCommand extends ContextMenuCommand {
public constructor() {
diff --git a/src/lib/common/AutoMod.ts b/src/lib/common/AutoMod.ts
deleted file mode 100644
index 44c6dee..0000000
--- a/src/lib/common/AutoMod.ts
+++ /dev/null
@@ -1,529 +0,0 @@
-import { colors, emojis, format, formatError, Moderation, unmuteResponse } from '#lib';
-import assert from 'assert/strict';
-import chalk from 'chalk';
-import {
- ActionRowBuilder,
- ButtonBuilder,
- ButtonStyle,
- EmbedBuilder,
- GuildMember,
- PermissionFlagsBits,
- type ButtonInteraction,
- type Message,
- type Snowflake,
- type TextChannel
-} from 'discord.js';
-import UnmuteCommand from '../../commands/moderation/unmute.js';
-
-/**
- * Handles auto moderation functionality.
- */
-export class AutoMod {
- /**
- * Whether or not a punishment has already been given to the user
- */
- private punished = false;
-
- /**
- * @param message The message to check and potentially perform automod actions to
- */
- public constructor(private message: Message) {
- if (message.author.id === message.client.user?.id) return;
- void this.handle();
- }
-
- /**
- * Whether or not the message author is immune to auto moderation
- */
- private get isImmune() {
- if (!this.message.inGuild()) return false;
- assert(this.message.member);
-
- if (this.message.author.isOwner()) return true;
- if (this.message.guild.ownerId === this.message.author.id) return true;
- if (this.message.member.permissions.has('Administrator')) return true;
-
- return false;
- }
-
- /**
- * Handles the auto moderation
- */
- private async handle() {
- if (!this.message.inGuild()) return;
- if (!(await this.message.guild.hasFeature('automod'))) return;
- if (this.message.author.bot) return;
-
- traditional: {
- if (this.isImmune) break traditional;
- const badLinksArray = this.message.client.utils.getShared('badLinks');
- const badLinksSecretArray = this.message.client.utils.getShared('badLinksSecret');
- const badWordsRaw = this.message.client.utils.getShared('badWords');
-
- const customAutomodPhrases = (await this.message.guild.getSetting('autoModPhases')) ?? [];
- const uniqueLinks = [...new Set([...badLinksArray, ...badLinksSecretArray])];
-
- const badLinks: BadWordDetails[] = uniqueLinks.map((link) => ({
- match: link,
- severity: Severity.PERM_MUTE,
- ignoreSpaces: false,
- ignoreCapitalization: true,
- reason: 'malicious link',
- regex: false
- }));
-
- const parsedBadWords = Object.values(badWordsRaw).flat();
-
- const result = [
- ...this.checkWords(customAutomodPhrases),
- ...this.checkWords((await this.message.guild.hasFeature('excludeDefaultAutomod')) ? [] : parsedBadWords),
- ...this.checkWords((await this.message.guild.hasFeature('excludeAutomodScamLinks')) ? [] : badLinks)
- ];
-
- if (result.length === 0) break traditional;
-
- const highestOffence = result.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 ${format.inlineCode(highestOffence.match)}`,
- color: colors.error
- }
- ]
- });
- } else {
- const color = this.punish(highestOffence);
- void this.log(highestOffence, color, result);
- }
- }
-
- other: {
- if (this.isImmune) break other;
- if (!this.punished && (await this.message.guild.hasFeature('delScamMentions'))) void this.checkScamMentions();
- }
-
- if (!this.punished && (await this.message.guild.hasFeature('perspectiveApi'))) void this.checkPerspectiveApi();
- }
-
- /**
- * Checks if any of the words provided are in the message
- * @param words The words to check for
- * @returns The blacklisted words found in the message
- */
- private checkWords(words: BadWordDetails[]): BadWordDetails[] {
- if (words.length === 0) return [];
-
- const matchedWords: BadWordDetails[] = [];
- for (const word of words) {
- if (word.regex) {
- if (new RegExp(word.match).test(this.format(word.match, word))) {
- matchedWords.push(word);
- }
- } else {
- if (this.format(this.message.content, word).includes(this.format(word.match, word))) {
- matchedWords.push(word);
- }
- }
- }
- return matchedWords;
- }
-
- /**
- * If the message contains '@everyone' or '@here' and it contains a common scam phrase, it will be deleted
- * @returns
- */
- private async checkScamMentions() {
- const includes = (c: string) => this.message.content.toLocaleLowerCase().includes(c);
- if (!includes('@everyone') && !includes('@here')) return;
- // It would be bad if we deleted a message that actually pinged @everyone or @here
- if (
- this.message.member?.permissionsIn(this.message.channelId).has(PermissionFlagsBits.MentionEveryone) ||
- this.message.mentions.everyone
- )
- return;
-
- if (
- includes('steam') ||
- includes('www.youtube.com') ||
- includes('youtu.be') ||
- includes('nitro') ||
- includes('1 month') ||
- includes('3 months') ||
- includes('personalize your profile') ||
- includes('even more') ||
- includes('xbox and discord') ||
- includes('left over') ||
- includes('check this lol') ||
- includes('airdrop')
- ) {
- const color = this.punish({ severity: Severity.TEMP_MUTE, reason: 'everyone mention and scam phrase' } as BadWordDetails);
- void this.message.guild!.sendLogChannel('automod', {
- embeds: [
- new EmbedBuilder()
- .setTitle(`[Severity ${Severity.TEMP_MUTE}] Mention Scam Deleted`)
- .setDescription(
- `**User:** ${this.message.author} (${this.message.author.tag})\n**Sent From:** <#${this.message.channel.id}> [Jump to context](${this.message.url})`
- )
- .addFields({
- name: 'Message Content',
- value: `${await this.message.client.utils.codeblock(this.message.content, 1024)}`
- })
- .setColor(color)
- .setTimestamp()
- ],
- components: [this.buttons(this.message.author.id, 'everyone mention and scam phrase')]
- });
- }
- }
-
- private async checkPerspectiveApi() {
- return;
- if (!this.message.client.config.isDevelopment) return;
-
- if (!this.message.content) return;
- this.message.client.perspective.comments.analyze(
- {
- key: this.message.client.config.credentials.perspectiveApiKey,
- resource: {
- comment: {
- text: this.message.content
- },
- requestedAttributes: {
- TOXICITY: {},
- SEVERE_TOXICITY: {},
- IDENTITY_ATTACK: {},
- INSULT: {},
- PROFANITY: {},
- THREAT: {},
- SEXUALLY_EXPLICIT: {},
- FLIRTATION: {}
- }
- }
- },
- (err: any, response: any) => {
- if (err) return console.log(err?.message);
-
- const normalize = (val: number, min: number, max: number) => (val - min) / (max - min);
-
- const color = (val: number) => {
- if (val >= 0.5) {
- const x = 194 - Math.round(normalize(val, 0.5, 1) * 194);
- return chalk.rgb(194, x, 0)(val);
- } else {
- const x = Math.round(normalize(val, 0, 0.5) * 194);
- return chalk.rgb(x, 194, 0)(val);
- }
- };
-
- console.log(chalk.cyan(this.message.content));
- Object.entries(response.data.attributeScores)
- .sort(([a], [b]) => a.localeCompare(b))
- .forEach(([key, value]: any[]) => console.log(chalk.white(key), color(value.summaryScore.value)));
- }
- );
- }
-
- /**
- * Format a string according to the word options
- * @param string The string to format
- * @param wordOptions The word options to format with
- * @returns The formatted string
- */
- private format(string: string, wordOptions: BadWordDetails) {
- const temp = wordOptions.ignoreCapitalization ? string.toLowerCase() : string;
- return wordOptions.ignoreSpaces ? temp.replace(/ /g, '') : temp;
- }
-
- /**
- * Punishes the user based on the severity of the offense
- * @param highestOffence The highest offense to punish the user for
- * @returns The color of the embed that the log should, based on the severity of the offense
- */
- private punish(highestOffence: BadWordDetails) {
- let color;
- switch (highestOffence.severity) {
- case Severity.DELETE: {
- color = colors.lightGray;
- void this.message.delete().catch((e) => deleteError.bind(this, e));
- this.punished = true;
- break;
- }
- case Severity.WARN: {
- color = colors.yellow;
- void this.message.delete().catch((e) => deleteError.bind(this, e));
- void this.message.member?.bushWarn({
- moderator: this.message.guild!.members.me!,
- reason: `[AutoMod] ${highestOffence.reason}`
- });
- this.punished = true;
- break;
- }
- case Severity.TEMP_MUTE: {
- color = colors.orange;
- void this.message.delete().catch((e) => deleteError.bind(this, e));
- void this.message.member?.bushMute({
- moderator: this.message.guild!.members.me!,
- reason: `[AutoMod] ${highestOffence.reason}`,
- duration: 900_000 // 15 minutes
- });
- this.punished = true;
- break;
- }
- case Severity.PERM_MUTE: {
- color = colors.red;
- void this.message.delete().catch((e) => deleteError.bind(this, e));
- void this.message.member?.bushMute({
- moderator: this.message.guild!.members.me!,
- reason: `[AutoMod] ${highestOffence.reason}`,
- duration: 0 // permanent
- });
- this.punished = true;
- break;
- }
- default: {
- throw new Error(`Invalid severity: ${highestOffence.severity}`);
- }
- }
-
- return color;
-
- async function deleteError(this: AutoMod, e: Error | any) {
- void this.message.guild?.sendLogChannel('error', {
- embeds: [
- {
- title: 'AutoMod Error',
- description: `Unable to delete triggered message.`,
- fields: [{ name: 'Error', value: await this.message.client.utils.codeblock(`${formatError(e)}`, 1024, 'js', true) }],
- color: colors.error
- }
- ]
- });
- }
- }
-
- /**
- * Log an automod infraction to the guild's specified automod log channel
- * @param highestOffence The highest severity word found in the message
- * @param color The color that the log embed should be (based on the severity)
- * @param offenses The other offenses that were also matched in the message
- */
- private async log(highestOffence: BadWordDetails, color: number, offenses: BadWordDetails[]) {
- void this.message.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}>>`
- );
-
- await this.message.guild!.sendLogChannel('automod', {
- embeds: [
- new EmbedBuilder()
- .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:** ${offenses.map((o) => `\`${o.match}\``).join(', ')}`
- )
- .addFields({
- name: 'Message Content',
- value: `${await this.message.client.utils.codeblock(this.message.content, 1024)}`
- })
- .setColor(color)
- .setTimestamp()
- .setAuthor({ name: this.message.author.tag, url: this.message.author.displayAvatarURL() })
- ],
- components: highestOffence.severity >= 2 ? [this.buttons(this.message.author.id, highestOffence.reason)] : undefined
- });
- }
-
- private buttons(userId: Snowflake, reason: string): ActionRowBuilder<ButtonBuilder> {
- return new ActionRowBuilder<ButtonBuilder>().addComponents(
- new ButtonBuilder({
- style: ButtonStyle.Danger,
- label: 'Ban User',
- customId: `automod;ban;${userId};${reason}`
- }),
- new ButtonBuilder({
- style: ButtonStyle.Success,
- label: 'Unmute User',
- customId: `automod;unmute;${userId}`
- })
- );
- }
-
- /**
- * Handles the ban button in the automod log.
- * @param interaction The button interaction.
- */
- public static async handleInteraction(interaction: ButtonInteraction) {
- if (!interaction.memberPermissions?.has(PermissionFlagsBits.BanMembers))
- return interaction.reply({
- content: `${emojis.error} You are missing the **Ban Members** permission.`,
- ephemeral: true
- });
- const [action, userId, reason] = interaction.customId.replace('automod;', '').split(';') as [
- 'ban' | 'unmute',
- string,
- string
- ];
-
- if (!(['ban', 'unmute'] as const).includes(action)) throw new TypeError(`Invalid automod button action: ${action}`);
-
- const victim = await interaction.guild!.members.fetch(userId).catch(() => null);
- const moderator =
- interaction.member instanceof GuildMember
- ? interaction.member
- : await interaction.guild!.members.fetch(interaction.user.id);
-
- switch (action) {
- case 'ban': {
- if (!interaction.guild?.members.me?.permissions.has('BanMembers'))
- return interaction.reply({
- content: `${emojis.error} I do not have permission to ${action} members.`,
- ephemeral: true
- });
-
- const check = victim ? await Moderation.permissionCheck(moderator, victim, 'ban', true) : true;
- if (check !== true) return interaction.reply({ content: check, ephemeral: true });
-
- const result = await interaction.guild?.bushBan({
- user: userId,
- reason,
- moderator: interaction.user.id,
- evidence: (interaction.message as Message).url ?? undefined
- });
-
- const victimUserFormatted = (await interaction.client.utils.resolveNonCachedUser(userId))?.tag ?? userId;
-
- const content = (() => {
- if (result === unmuteResponse.SUCCESS) {
- return `${emojis.success} Successfully banned ${format.input(victimUserFormatted)}.`;
- } else if (result === unmuteResponse.DM_ERROR) {
- return `${emojis.warn} Banned ${format.input(victimUserFormatted)} however I could not send them a dm.`;
- } else {
- return `${emojis.error} Could not ban ${format.input(victimUserFormatted)}: \`${result}\` .`;
- }
- })();
-
- return interaction.reply({
- content: content,
- ephemeral: true
- });
- }
-
- case 'unmute': {
- if (!victim)
- return interaction.reply({
- content: `${emojis.error} Cannot find member, they may have left the server.`,
- ephemeral: true
- });
-
- if (!interaction.guild)
- return interaction.reply({
- content: `${emojis.error} This is weird, I don't seem to be in the server...`,
- ephemeral: true
- });
-
- const check = await Moderation.permissionCheck(moderator, victim, 'unmute', true);
- if (check !== true) return interaction.reply({ content: check, ephemeral: true });
-
- const check2 = await Moderation.checkMutePermissions(interaction.guild);
- if (check2 !== true)
- return interaction.reply({ content: UnmuteCommand.formatCode('/', victim!, check2), ephemeral: true });
-
- const result = await victim.bushUnmute({
- reason,
- moderator: interaction.member as GuildMember,
- evidence: (interaction.message as Message).url ?? undefined
- });
-
- const victimUserFormatted = victim.user.tag;
-
- const content = (() => {
- if (result === unmuteResponse.SUCCESS) {
- return `${emojis.success} Successfully unmuted ${format.input(victimUserFormatted)}.`;
- } else if (result === unmuteResponse.DM_ERROR) {
- return `${emojis.warn} Unmuted ${format.input(victimUserFormatted)} however I could not send them a dm.`;
- } else {
- return `${emojis.error} Could not unmute ${format.input(victimUserFormatted)}: \`${result}\` .`;
- }
- })();
-
- return interaction.reply({
- content: content,
- ephemeral: true
- });
- }
- }
- }
-}
-
-/**
- * The severity of the blacklisted word
- */
-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
-}
-
-/**
- * Details about a blacklisted word
- */
-export interface BadWordDetails {
- /**
- * The word that is blacklisted
- */
- match: string;
-
- /**
- * The severity of the word
- */
- severity: Severity | 1 | 2 | 3;
-
- /**
- * Whether or not to ignore spaces when checking for the word
- */
- ignoreSpaces: boolean;
-
- /**
- * Whether or not to ignore case when checking for the word
- */
- ignoreCapitalization: boolean;
-
- /**
- * The reason that this word is blacklisted (used for the punishment reason)
- */
- reason: string;
-
- /**
- * Whether or not the word is regex
- */
- regex: boolean;
-}
-
-/**
- * Blacklisted words mapped to their details
- */
-export interface BadWords {
- [category: string]: BadWordDetails[];
-}
diff --git a/src/listeners/message/automodCreate.ts b/src/listeners/automod/automodCreate.ts
index 0f089a3..529651c 100644
--- a/src/listeners/message/automodCreate.ts
+++ b/src/listeners/automod/automodCreate.ts
@@ -1,4 +1,4 @@
-import { AutoMod, BushListener, type BushClientEvents } from '#lib';
+import { BushListener, MessageAutomod, type BushClientEvents } from '#lib';
export default class AutomodMessageCreateListener extends BushListener {
public constructor() {
@@ -10,6 +10,7 @@ export default class AutomodMessageCreateListener extends BushListener {
}
public async exec(...[message]: BushClientEvents['messageCreate']) {
- return new AutoMod(message);
+ if (message.member === null) return;
+ return new MessageAutomod(message);
}
}
diff --git a/src/listeners/message/automodUpdate.ts b/src/listeners/automod/automodUpdate.ts
index fa00f92..d68759f 100644
--- a/src/listeners/message/automodUpdate.ts
+++ b/src/listeners/automod/automodUpdate.ts
@@ -1,4 +1,4 @@
-import { AutoMod, BushListener, type BushClientEvents } from '#lib';
+import { BushListener, MessageAutomod, type BushClientEvents } from '#lib';
export default class AutomodMessageUpdateListener extends BushListener {
public constructor() {
@@ -11,7 +11,7 @@ export default class AutomodMessageUpdateListener extends BushListener {
public async exec(...[_, newMessage]: BushClientEvents['messageUpdate']) {
const fullMessage = newMessage.partial ? await newMessage.fetch().catch(() => null) : newMessage;
- if (!fullMessage) return;
- return new AutoMod(fullMessage);
+ if (!fullMessage?.member) return;
+ return new MessageAutomod(fullMessage);
}
}
diff --git a/src/listeners/automod/memberAutomod.ts b/src/listeners/automod/memberAutomod.ts
new file mode 100644
index 0000000..01eb56c
--- /dev/null
+++ b/src/listeners/automod/memberAutomod.ts
@@ -0,0 +1,21 @@
+import { BushClientEvents, BushListener, MemberAutomod } from '#lib';
+import chalk from 'chalk';
+
+export default class PresenceAutomodListener extends BushListener {
+ public constructor() {
+ super('memberAutomod', {
+ emitter: 'client',
+ event: 'guildMemberUpdate'
+ });
+ }
+
+ public async exec(...[_, newMember]: BushClientEvents['guildMemberUpdate']) {
+ if (!(await newMember.guild.hasFeature('automodMembers'))) return;
+ if (!(await newMember.guild.hasFeature('automod'))) return;
+
+ new MemberAutomod(newMember);
+ console.log(
+ `${chalk.hex('#ff7105')('[MemberAutomod]')} Created a new MemberAutomod for ${newMember.user.tag} (${newMember.user.id})`
+ );
+ }
+}
diff --git a/src/listeners/automod/presenceAutomod.ts b/src/listeners/automod/presenceAutomod.ts
new file mode 100644
index 0000000..f361508
--- /dev/null
+++ b/src/listeners/automod/presenceAutomod.ts
@@ -0,0 +1,27 @@
+import { BushClientEvents, BushListener, PresenceAutomod } from '#lib';
+import chalk from 'chalk';
+
+/* export default */ class PresenceAutomodListener extends BushListener {
+ public constructor() {
+ super('presenceAutomod', {
+ emitter: 'client',
+ event: 'presenceUpdate'
+ });
+ }
+
+ public async exec(...[_, newPresence]: BushClientEvents['presenceUpdate']) {
+ if (!newPresence.member || !newPresence.guild) return;
+
+ if (!newPresence.activities.length) return;
+
+ if (!(await newPresence.guild.hasFeature('automodPresence'))) return;
+ if (!(await newPresence.guild.hasFeature('automod'))) return;
+
+ new PresenceAutomod(newPresence);
+ console.log(
+ `${chalk.hex('#ffe605')('[PresenceAutomod]')} Created a new PresenceAutomod for ${newPresence.member.user.tag} (${
+ newPresence.member.user.id
+ })`
+ );
+ }
+}
diff --git a/src/listeners/commands/commandError.ts b/src/listeners/commands/commandError.ts
index f12881a..bfa857c 100644
--- a/src/listeners/commands/commandError.ts
+++ b/src/listeners/commands/commandError.ts
@@ -1,8 +1,8 @@
import { capitalize, colors, format, formatError, SlashMessage, type BushCommandHandlerEvents } from '#lib';
import { type AkairoMessage, type Command } from 'discord-akairo';
import { ChannelType, Client, EmbedBuilder, escapeInlineCode, GuildTextBasedChannel, type Message } from 'discord.js';
-import { bold } from '../../lib/common/util/Format.js';
-import { BushListener } from '../../lib/extensions/discord-akairo/BushListener.js';
+import { BushListener } from '../../../lib/extensions/discord-akairo/BushListener.js';
+import { bold } from '../../../lib/utils/Format.js';
export default class CommandErrorListener extends BushListener {
public constructor() {
diff --git a/src/listeners/interaction/interactionCreate.ts b/src/listeners/interaction/interactionCreate.ts
index 91bcae6..8dd753b 100644
--- a/src/listeners/interaction/interactionCreate.ts
+++ b/src/listeners/interaction/interactionCreate.ts
@@ -1,4 +1,4 @@
-import { AutoMod, BushListener, emojis, format, oxford, surroundArray, type BushClientEvents } from '#lib';
+import { BushListener, emojis, format, handleAutomodInteraction, oxford, surroundArray, type BushClientEvents } from '#lib';
import { InteractionType } from 'discord.js';
export default class InteractionCreateListener extends BushListener {
@@ -22,7 +22,7 @@ export default class InteractionCreateListener extends BushListener {
} else if (interaction.isButton()) {
const id = interaction.customId;
if (['paginate_', 'command_', 'confirmationPrompt_', 'appeal'].some((s) => id.startsWith(s))) return;
- else if (id.startsWith('automod;')) void AutoMod.handleInteraction(interaction);
+ else if (id.startsWith('automod;')) void handleAutomodInteraction(interaction);
else if (id.startsWith('button-role;') && interaction.inCachedGuild()) {
const [, roleId] = id.split(';');
const role = interaction.guild.roles.cache.get(roleId);
diff --git a/src/listeners/member-custom/bushLevelUpdate.ts b/src/listeners/member-custom/bushLevelUpdate.ts
index 0281288..702f7cc 100644
--- a/src/listeners/member-custom/bushLevelUpdate.ts
+++ b/src/listeners/member-custom/bushLevelUpdate.ts
@@ -52,10 +52,12 @@ export default class BushLevelUpdateListener extends BushListener {
}
try {
if (promises.length) await Promise.all(promises);
- } catch (e) {
+ } catch (e: any) {
await member.guild.error(
'bushLevelUpdate',
- `There was an error adding level roles to ${member.user.tag} upon reaching to level ${newLevel}.\n${e?.message ?? e}`
+ `There was an error adding level roles to ${member.user.tag} upon reaching to level ${newLevel}.\n${
+ 'message' in e ? e.message : e
+ }`
);
}
}
diff --git a/src/tasks/cache/updateCache.ts b/src/tasks/cache/updateCache.ts
index 87636e8..595a872 100644
--- a/src/tasks/cache/updateCache.ts
+++ b/src/tasks/cache/updateCache.ts
@@ -1,8 +1,8 @@
-import { Time } from '#constants';
import { Global, Guild, Shared, type BushClient } from '#lib';
import type { Client } from 'discord.js';
import config from '../../../config/options.js';
-import { BushTask } from '../../lib/extensions/discord-akairo/BushTask.js';
+import { BushTask } from '../../../lib/extensions/discord-akairo/BushTask.js';
+import { Time } from '../../../lib/utils/BushConstants.js';
export default class UpdateCacheTask extends BushTask {
public constructor() {
diff --git a/src/tasks/cache/updateHighlightCache.ts b/src/tasks/cache/updateHighlightCache.ts
index 44ddd90..4ab5544 100644
--- a/src/tasks/cache/updateHighlightCache.ts
+++ b/src/tasks/cache/updateHighlightCache.ts
@@ -1,5 +1,5 @@
-import { Time } from '#constants';
-import { BushTask } from '../../lib/extensions/discord-akairo/BushTask.js';
+import { BushTask } from '../../../lib/extensions/discord-akairo/BushTask.js';
+import { Time } from '../../../lib/utils/BushConstants.js';
export default class UpdateHighlightCacheTask extends BushTask {
public constructor() {
diff --git a/src/tasks/cache/updatePriceItemCache.ts b/src/tasks/cache/updatePriceItemCache.ts
index 9809cbd..55115cc 100644
--- a/src/tasks/cache/updatePriceItemCache.ts
+++ b/src/tasks/cache/updatePriceItemCache.ts
@@ -12,7 +12,12 @@ export default class UpdatePriceItemCache extends BushTask {
public async exec() {
const [bazaar, currentLowestBIN, averageLowestBIN, auctionAverages] = (await Promise.all(
- PriceCommand.urls.map(({ url }) => got.get(url).json().catch(undefined))
+ PriceCommand.urls.map(({ url }) =>
+ got
+ .get(url)
+ .json()
+ .catch(() => undefined)
+ )
)) as [Bazaar?, LowestBIN?, LowestBIN?, AuctionAverages?];
const itemNames = new Set([
diff --git a/src/tasks/feature/handleReminders.ts b/src/tasks/feature/handleReminders.ts
index 7863c9a..1e44083 100644
--- a/src/tasks/feature/handleReminders.ts
+++ b/src/tasks/feature/handleReminders.ts
@@ -29,7 +29,8 @@ export default class HandlerRemindersTask extends BushTask {
void this.client.users
.send(
entry.user,
- `The reminder you set ${dateDelta(entry.created)} ago has expired: ${format.bold(entry.content)}\n${entry.messageUrl}`
+ `The reminder you set ${dateDelta(entry.created)} ago has expired: ${format.bold(entry.content)}
+${entry.messageUrl}`
)
.catch(() => false);
void entry.update({ notified: true });
diff --git a/src/tasks/stats/guildCount.ts b/src/tasks/stats/guildCount.ts
index 262f00c..f52dc95 100644
--- a/src/tasks/stats/guildCount.ts
+++ b/src/tasks/stats/guildCount.ts
@@ -1,5 +1,5 @@
import { BushTask, Time } from '#lib';
-import { GuildCount } from '../../lib/models/shared/GuildCount.js';
+import { GuildCount } from '../../../lib/models/shared/GuildCount.js';
export default class GuildCountTask extends BushTask {
public constructor() {
diff --git a/src/tsconfig.json b/src/tsconfig.json
new file mode 100644
index 0000000..6d2834a
--- /dev/null
+++ b/src/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "../../dist/src",
+ "composite": true
+ },
+ "references": [{ "path": "../lib" }, { "path": "../config" }],
+ "include": ["src/**/*.ts"]
+}
diff --git a/tests/arguments/abbreviatedNumber.test.ts b/tests/arguments/abbreviatedNumber.test.ts
index 2cf326d..cd34bc5 100644
--- a/tests/arguments/abbreviatedNumber.test.ts
+++ b/tests/arguments/abbreviatedNumber.test.ts
@@ -1,6 +1,6 @@
+import { CommandMessage } from '#lib';
import { expect, test } from 'vitest';
-import { abbreviatedNumber } from '../../src/arguments/abbreviatedNumber.js';
-import { CommandMessage } from '../../src/lib/index.js';
+import { abbreviatedNumber } from '../../lib/arguments/abbreviatedNumber.js';
const message = {} as CommandMessage;
diff --git a/tsconfig.base.json b/tsconfig.base.json
new file mode 100644
index 0000000..44301c8
--- /dev/null
+++ b/tsconfig.base.json
@@ -0,0 +1,67 @@
+{
+ "compilerOptions": {
+ // #region Projects
+ "incremental": true,
+ "composite": true,
+ // #endregion
+
+ // #region Language and Environment
+ "target": "ESNext",
+ "lib": ["ESNext"],
+ "experimentalDecorators": true,
+ "emitDecoratorMetadata": true,
+ "useDefineForClassFields": true,
+ // #endregion
+
+ // #region Modules
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ // "baseUrl": "./",
+ "paths": {
+ "#lib": ["./lib/index.ts"],
+ "#constants": ["./lib/utils/BushConstants.ts"],
+ "#args": ["./lib/arguments/index.ts"],
+ "#commands": ["./src/commands/index.ts"],
+ "#tags": ["./lib/common/tags.ts"]
+ },
+ "resolveJsonModule": true,
+ // "noResolve": true,
+ // #endregion
+
+ // #region JavaScript Support
+ "allowJs": false,
+ "checkJs": false,
+ "maxNodeModuleJsDepth": 0,
+ // #endregion
+
+ // #region Emit
+ "sourceMap": true,
+ "removeComments": true,
+ "importsNotUsedAsValues": "remove",
+ "newLine": "lf",
+ "noEmitOnError": true,
+ "preserveConstEnums": true,
+ "preserveValueImports": true,
+ // #endregion
+
+ // #region Interop Constraints
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ // #endregion
+
+ // #region Type Checking
+ "strict": true,
+ "useUnknownInCatchVariables": true,
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitOverride": true,
+ "allowUnusedLabels": true,
+ "allowUnreachableCode": true,
+ // #endregion
+
+ // #region Completeness
+ "skipDefaultLibCheck": false,
+ "skipLibCheck": true
+ // #endregion
+ }
+}
diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json
index 59f6ecc..8b91633 100644
--- a/tsconfig.eslint.json
+++ b/tsconfig.eslint.json
@@ -1,15 +1,8 @@
{
- "extends": "./tsconfig.json",
- "include": [
- "src/**/*.ts",
- "src/**/*d.ts",
- "lib/**/*.ts",
- "ecosystem.config.cjs",
- ".eslintrc.cjs",
- "config/**/*.ts",
- "tests/**/*.ts",
- "vite.config.ts",
- "tooltips*",
- "test*"
- ]
+ "extends": "./tsconfig.base.json",
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": true
+ },
+ "include": ["**/*"]
}
diff --git a/tsconfig.json b/tsconfig.json
index 6d24566..0310e3d 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,32 +1,9 @@
{
+ "extends": "./tsconfig.base.json",
"compilerOptions": {
- "module": "ESNext",
- "target": "ESNext",
- "moduleResolution": "Node",
- "outDir": "dist",
- "lib": ["ESNext"],
- "sourceMap": true,
- "incremental": true,
- "experimentalDecorators": true,
- "emitDecoratorMetadata": true,
- "resolveJsonModule": true,
- "noImplicitOverride": true,
- "noErrorTruncation": true,
- "strict": true,
- "baseUrl": "./",
- "useUnknownInCatchVariables": false,
- "forceConsistentCasingInFileNames": true,
- "allowSyntheticDefaultImports": true,
- "preserveValueImports": true,
- "removeComments": true,
- "paths": {
- "#lib": ["./src/lib/index.ts"],
- "#constants": ["./src/lib/utils/BushConstants.ts"],
- "#args": ["./src/arguments/index.ts"],
- "#commands": ["./src/commands/index.ts"],
- "#tags": ["./src/lib/common/tags.js"]
- }
+ "outDir": "./dist"
},
- "include": ["src/**/*.ts", "src/**/*d.ts", "lib/**/*.ts", "test.js"],
- "exclude": ["dist", "node_modules"]
+ "references": [{ "path": "./src" }, { "path": "./lib" }, { "path": "./config" }],
+ "include": ["./src/**/*.ts", "./lib/**/*.ts"],
+ "files": ["./package.json", "config/Config.ts", "config/example-options.ts", "config/options.ts"]
}