diff options
-rw-r--r-- | .eslintrc.cjs | 2 | ||||
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | .vscode/settings.json | 5 | ||||
-rw-r--r-- | config/tsconfig.json | 7 | ||||
-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.ts | 310 | ||||
-rw-r--r-- | lib/automod/MemberAutomod.ts | 72 | ||||
-rw-r--r-- | lib/automod/MessageAutomod.ts | 286 | ||||
-rw-r--r-- | lib/automod/PresenceAutomod.ts | 85 | ||||
-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.json | 9 | ||||
-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) | bin | 41015 -> 41015 bytes | |||
-rw-r--r-- | misc/tooltips.nnb (renamed from tooltips.nnb) | 0 | ||||
-rw-r--r-- | package.json | 14 | ||||
-rw-r--r-- | src/bot.ts | 6 | ||||
-rw-r--r-- | src/commands/admin/channelPermissions.ts | 2 | ||||
-rw-r--r-- | src/commands/dev/test.ts | 6 | ||||
-rw-r--r-- | src/commands/info/help.ts | 2 | ||||
-rw-r--r-- | src/commands/moderation/massEvidence.ts | 2 | ||||
-rw-r--r-- | src/commands/moderation/myLogs.ts | 2 | ||||
-rw-r--r-- | src/commands/moderation/unmute.ts | 2 | ||||
-rw-r--r-- | src/commands/moulberry-bush/neuRepo.ts | 2 | ||||
-rw-r--r-- | src/commands/moulberry-bush/rule.ts | 2 | ||||
-rw-r--r-- | src/commands/utilities/calculator.ts | 2 | ||||
-rw-r--r-- | src/commands/utilities/highlight-block.ts | 2 | ||||
-rw-r--r-- | src/commands/utilities/highlight-unblock.ts | 2 | ||||
-rw-r--r-- | src/commands/utilities/uuid.ts | 2 | ||||
-rw-r--r-- | src/commands/utilities/wolframAlpha.ts | 2 | ||||
-rw-r--r-- | src/context-menu-commands/message/viewRaw.ts | 2 | ||||
-rw-r--r-- | src/context-menu-commands/user/modlog.ts | 2 | ||||
-rw-r--r-- | src/context-menu-commands/user/userInfo.ts | 2 | ||||
-rw-r--r-- | src/lib/common/AutoMod.ts | 529 | ||||
-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.ts | 21 | ||||
-rw-r--r-- | src/listeners/automod/presenceAutomod.ts | 27 | ||||
-rw-r--r-- | src/listeners/commands/commandError.ts | 4 | ||||
-rw-r--r-- | src/listeners/interaction/interactionCreate.ts | 4 | ||||
-rw-r--r-- | src/listeners/member-custom/bushLevelUpdate.ts | 6 | ||||
-rw-r--r-- | src/tasks/cache/updateCache.ts | 4 | ||||
-rw-r--r-- | src/tasks/cache/updateHighlightCache.ts | 4 | ||||
-rw-r--r-- | src/tasks/cache/updatePriceItemCache.ts | 7 | ||||
-rw-r--r-- | src/tasks/feature/handleReminders.ts | 3 | ||||
-rw-r--r-- | src/tasks/stats/guildCount.ts | 2 | ||||
-rw-r--r-- | src/tsconfig.json | 9 | ||||
-rw-r--r-- | tests/arguments/abbreviatedNumber.test.ts | 4 | ||||
-rw-r--r-- | tsconfig.base.json | 67 | ||||
-rw-r--r-- | tsconfig.eslint.json | 19 | ||||
-rw-r--r-- | tsconfig.json | 33 |
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', @@ -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 Binary files differindex 26ca648..26ca648 100644 --- a/src/lib/common/util/Minecraft_Test.ts +++ b/lib/utils/Minecraft_Test.ts 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": { @@ -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"] } |