diff options
author | IRONM00N <64110067+IRONM00N@users.noreply.github.com> | 2022-06-16 14:32:18 -0400 |
---|---|---|
committer | IRONM00N <64110067+IRONM00N@users.noreply.github.com> | 2022-06-16 14:32:18 -0400 |
commit | 0e87bbd3940d89defcb04926587b35c8f4d1947f (patch) | |
tree | e50860d4dc25a11d4c3977b583284c4bcad1b077 /src/lib/common/util | |
parent | 661e4c9935aeb8760dafc7ced4bbec6cc356a033 (diff) | |
download | tanzanite-0e87bbd3940d89defcb04926587b35c8f4d1947f.tar.gz tanzanite-0e87bbd3940d89defcb04926587b35c8f4d1947f.tar.bz2 tanzanite-0e87bbd3940d89defcb04926587b35c8f4d1947f.zip |
remove util classes, move config out of src
Diffstat (limited to 'src/lib/common/util')
-rw-r--r-- | src/lib/common/util/Arg.ts | 269 | ||||
-rw-r--r-- | src/lib/common/util/Format.ts | 187 | ||||
-rw-r--r-- | src/lib/common/util/Moderation.ts | 517 |
3 files changed, 490 insertions, 483 deletions
diff --git a/src/lib/common/util/Arg.ts b/src/lib/common/util/Arg.ts index 51d8065..a7795b1 100644 --- a/src/lib/common/util/Arg.ts +++ b/src/lib/common/util/Arg.ts @@ -9,155 +9,150 @@ import { Argument, type Flag, type ParsedValuePredicate } from 'discord-akairo'; import { type Message } from 'discord.js'; /** - * A wrapper for the {@link Argument} class that adds custom typings. + * Casts a phrase to this argument's type. + * @param type - The type to cast to. + * @param message - Message that called the command. + * @param phrase - Phrase to process. */ -export class Arg { - /** - * Casts a phrase to this argument's type. - * @param type - The type to cast to. - * @param message - Message that called the command. - * @param phrase - Phrase to process. - */ - public static async cast<T extends ATC>(type: T, message: CommandMessage | SlashMessage, phrase: string): Promise<ATCR<T>>; - public static async cast<T extends KBAT>(type: T, message: CommandMessage | SlashMessage, phrase: string): Promise<BAT[T]>; - public static async cast(type: AT | ATC, message: CommandMessage | SlashMessage, phrase: string): Promise<any>; - public static async cast(type: ATC | AT, message: CommandMessage | SlashMessage, phrase: string): Promise<any> { - return Argument.cast(type as any, client.commandHandler.resolver, message as Message, phrase); - } +export async function cast<T extends ATC>(type: T, message: CommandMessage | SlashMessage, phrase: string): Promise<ATCR<T>>; +export async function cast<T extends KBAT>(type: T, message: CommandMessage | SlashMessage, phrase: string): Promise<BAT[T]>; +export async function cast(type: AT | ATC, message: CommandMessage | SlashMessage, phrase: string): Promise<any>; +export async function cast(type: ATC | AT, message: CommandMessage | SlashMessage, phrase: string): Promise<any> { + return Argument.cast(type as any, client.commandHandler.resolver, message as Message, phrase); +} - /** - * Creates a type that is the left-to-right composition of the given types. - * If any of the types fails, the entire composition fails. - * @param types - Types to use. - */ - public static compose<T extends ATC>(...types: T[]): ATCATCR<T>; - public static compose<T extends KBAT>(...types: T[]): ATCBAT<T>; - public static compose(...types: (AT | ATC)[]): ATC; - public static compose(...types: (AT | ATC)[]): ATC { - return Argument.compose(...(types as any)); - } +/** + * Creates a type that is the left-to-right composition of the given types. + * If any of the types fails, the entire composition fails. + * @param types - Types to use. + */ +export function compose<T extends ATC>(...types: T[]): ATCATCR<T>; +export function compose<T extends KBAT>(...types: T[]): ATCBAT<T>; +export function compose(...types: (AT | ATC)[]): ATC; +export function compose(...types: (AT | ATC)[]): ATC { + return Argument.compose(...(types as any)); +} - /** - * Creates a type that is the left-to-right composition of the given types. - * If any of the types fails, the composition still continues with the failure passed on. - * @param types - Types to use. - */ - public static composeWithFailure<T extends ATC>(...types: T[]): ATCATCR<T>; - public static composeWithFailure<T extends KBAT>(...types: T[]): ATCBAT<T>; - public static composeWithFailure(...types: (AT | ATC)[]): ATC; - public static composeWithFailure(...types: (AT | ATC)[]): ATC { - return Argument.composeWithFailure(...(types as any)); - } +/** + * Creates a type that is the left-to-right composition of the given types. + * If any of the types fails, the composition still continues with the failure passed on. + * @param types - Types to use. + */ +export function composeWithFailure<T extends ATC>(...types: T[]): ATCATCR<T>; +export function composeWithFailure<T extends KBAT>(...types: T[]): ATCBAT<T>; +export function composeWithFailure(...types: (AT | ATC)[]): ATC; +export function composeWithFailure(...types: (AT | ATC)[]): ATC { + return Argument.composeWithFailure(...(types as any)); +} - /** - * Checks if something is null, undefined, or a fail flag. - * @param value - Value to check. - */ - public static isFailure(value: any): value is null | undefined | (Flag & { value: any }) { - return Argument.isFailure(value); - } +/** + * Checks if something is null, undefined, or a fail flag. + * @param value - Value to check. + */ +export function isFailure(value: any): value is null | undefined | (Flag & { value: any }) { + return Argument.isFailure(value); +} - /** - * Creates a type from multiple types (product type). - * Only inputs where each type resolves with a non-void value are valid. - * @param types - Types to use. - */ - public static product<T extends ATC>(...types: T[]): ATCATCR<T>; - public static product<T extends KBAT>(...types: T[]): ATCBAT<T>; - public static product(...types: (AT | ATC)[]): ATC; - public static product(...types: (AT | ATC)[]): ATC { - return Argument.product(...(types as any)); - } +/** + * Creates a type from multiple types (product type). + * Only inputs where each type resolves with a non-void value are valid. + * @param types - Types to use. + */ +export function product<T extends ATC>(...types: T[]): ATCATCR<T>; +export function product<T extends KBAT>(...types: T[]): ATCBAT<T>; +export function product(...types: (AT | ATC)[]): ATC; +export function product(...types: (AT | ATC)[]): ATC { + return Argument.product(...(types as any)); +} - /** - * Creates a type where the parsed value must be within a range. - * @param type - The type to use. - * @param min - Minimum value. - * @param max - Maximum value. - * @param inclusive - Whether or not to be inclusive on the upper bound. - */ - public static range<T extends ATC>(type: T, min: number, max: number, inclusive?: boolean): ATCATCR<T>; - public static range<T extends KBAT>(type: T, min: number, max: number, inclusive?: boolean): ATCBAT<T>; - public static range(type: AT | ATC, min: number, max: number, inclusive?: boolean): ATC; - public static range(type: AT | ATC, min: number, max: number, inclusive?: boolean): ATC { - return Argument.range(type as any, min, max, inclusive); - } +/** + * Creates a type where the parsed value must be within a range. + * @param type - The type to use. + * @param min - Minimum value. + * @param max - Maximum value. + * @param inclusive - Whether or not to be inclusive on the upper bound. + */ +export function range<T extends ATC>(type: T, min: number, max: number, inclusive?: boolean): ATCATCR<T>; +export function range<T extends KBAT>(type: T, min: number, max: number, inclusive?: boolean): ATCBAT<T>; +export function range(type: AT | ATC, min: number, max: number, inclusive?: boolean): ATC; +export function range(type: AT | ATC, min: number, max: number, inclusive?: boolean): ATC { + return Argument.range(type as any, min, max, inclusive); +} - /** - * Creates a type that parses as normal but also tags it with some data. - * Result is in an object `{ tag, value }` and wrapped in `Flag.fail` when failed. - * @param type - The type to use. - * @param tag - Tag to add. Defaults to the `type` argument, so useful if it is a string. - */ - public static tagged<T extends ATC>(type: T, tag?: any): ATCATCR<T>; - public static tagged<T extends KBAT>(type: T, tag?: any): ATCBAT<T>; - public static tagged(type: AT | ATC, tag?: any): ATC; - public static tagged(type: AT | ATC, tag?: any): ATC { - return Argument.tagged(type as any, tag); - } +/** + * Creates a type that parses as normal but also tags it with some data. + * Result is in an object `{ tag, value }` and wrapped in `Flag.fail` when failed. + * @param type - The type to use. + * @param tag - Tag to add. Defaults to the `type` argument, so useful if it is a string. + */ +export function tagged<T extends ATC>(type: T, tag?: any): ATCATCR<T>; +export function tagged<T extends KBAT>(type: T, tag?: any): ATCBAT<T>; +export function tagged(type: AT | ATC, tag?: any): ATC; +export function tagged(type: AT | ATC, tag?: any): ATC { + return Argument.tagged(type as any, tag); +} - /** - * Creates a type from multiple types (union type). - * The first type that resolves to a non-void value is used. - * Each type will also be tagged using `tagged` with themselves. - * @param types - Types to use. - */ - public static taggedUnion<T extends ATC>(...types: T[]): ATCATCR<T>; - public static taggedUnion<T extends KBAT>(...types: T[]): ATCBAT<T>; - public static taggedUnion(...types: (AT | ATC)[]): ATC; - public static taggedUnion(...types: (AT | ATC)[]): ATC { - return Argument.taggedUnion(...(types as any)); - } +/** + * Creates a type from multiple types (union type). + * The first type that resolves to a non-void value is used. + * Each type will also be tagged using `tagged` with themselves. + * @param types - Types to use. + */ +export function taggedUnion<T extends ATC>(...types: T[]): ATCATCR<T>; +export function taggedUnion<T extends KBAT>(...types: T[]): ATCBAT<T>; +export function taggedUnion(...types: (AT | ATC)[]): ATC; +export function taggedUnion(...types: (AT | ATC)[]): ATC { + return Argument.taggedUnion(...(types as any)); +} - /** - * Creates a type that parses as normal but also tags it with some data and carries the original input. - * Result is in an object `{ tag, input, value }` and wrapped in `Flag.fail` when failed. - * @param type - The type to use. - * @param tag - Tag to add. Defaults to the `type` argument, so useful if it is a string. - */ - public static taggedWithInput<T extends ATC>(type: T, tag?: any): ATCATCR<T>; - public static taggedWithInput<T extends KBAT>(type: T, tag?: any): ATCBAT<T>; - public static taggedWithInput(type: AT | ATC, tag?: any): ATC; - public static taggedWithInput(type: AT | ATC, tag?: any): ATC { - return Argument.taggedWithInput(type as any, tag); - } +/** + * Creates a type that parses as normal but also tags it with some data and carries the original input. + * Result is in an object `{ tag, input, value }` and wrapped in `Flag.fail` when failed. + * @param type - The type to use. + * @param tag - Tag to add. Defaults to the `type` argument, so useful if it is a string. + */ +export function taggedWithInput<T extends ATC>(type: T, tag?: any): ATCATCR<T>; +export function taggedWithInput<T extends KBAT>(type: T, tag?: any): ATCBAT<T>; +export function taggedWithInput(type: AT | ATC, tag?: any): ATC; +export function taggedWithInput(type: AT | ATC, tag?: any): ATC { + return Argument.taggedWithInput(type as any, tag); +} - /** - * Creates a type from multiple types (union type). - * The first type that resolves to a non-void value is used. - * @param types - Types to use. - */ - public static union<T extends ATC>(...types: T[]): ATCATCR<T>; - public static union<T extends KBAT>(...types: T[]): ATCBAT<T>; - public static union(...types: (AT | ATC)[]): ATC; - public static union(...types: (AT | ATC)[]): ATC { - return Argument.union(...(types as any)); - } +/** + * Creates a type from multiple types (union type). + * The first type that resolves to a non-void value is used. + * @param types - Types to use. + */ +export function union<T extends ATC>(...types: T[]): ATCATCR<T>; +export function union<T extends KBAT>(...types: T[]): ATCBAT<T>; +export function union(...types: (AT | ATC)[]): ATC; +export function union(...types: (AT | ATC)[]): ATC { + return Argument.union(...(types as any)); +} - /** - * Creates a type with extra validation. - * If the predicate is not true, the value is considered invalid. - * @param type - The type to use. - * @param predicate - The predicate function. - */ - public static validate<T extends ATC>(type: T, predicate: ParsedValuePredicate): ATCATCR<T>; - public static validate<T extends KBAT>(type: T, predicate: ParsedValuePredicate): ATCBAT<T>; - public static validate(type: AT | ATC, predicate: ParsedValuePredicate): ATC; - public static validate(type: AT | ATC, predicate: ParsedValuePredicate): ATC { - return Argument.validate(type as any, predicate); - } +/** + * Creates a type with extra validation. + * If the predicate is not true, the value is considered invalid. + * @param type - The type to use. + * @param predicate - The predicate function. + */ +export function validate<T extends ATC>(type: T, predicate: ParsedValuePredicate): ATCATCR<T>; +export function validate<T extends KBAT>(type: T, predicate: ParsedValuePredicate): ATCBAT<T>; +export function validate(type: AT | ATC, predicate: ParsedValuePredicate): ATC; +export function validate(type: AT | ATC, predicate: ParsedValuePredicate): ATC { + return Argument.validate(type as any, predicate); +} - /** - * Creates a type that parses as normal but also carries the original input. - * Result is in an object `{ input, value }` and wrapped in `Flag.fail` when failed. - * @param type - The type to use. - */ - public static withInput<T extends ATC>(type: T): ATC<ATCR<T>>; - public static withInput<T extends KBAT>(type: T): ATCBAT<T>; - public static withInput(type: AT | ATC): ATC; - public static withInput(type: AT | ATC): ATC { - return Argument.withInput(type as any); - } +/** + * Creates a type that parses as normal but also carries the original input. + * Result is in an object `{ input, value }` and wrapped in `Flag.fail` when failed. + * @param type - The type to use. + */ +export function withInput<T extends ATC>(type: T): ATC<ATCR<T>>; +export function withInput<T extends KBAT>(type: T): ATCBAT<T>; +export function withInput(type: AT | ATC): ATC; +export function withInput(type: AT | ATC): ATC { + return Argument.withInput(type as any); } type BushArgumentTypeCasterReturn<R> = R extends BushArgumentTypeCaster<infer S> ? S : R; diff --git a/src/lib/common/util/Format.ts b/src/lib/common/util/Format.ts index 6cb6edc..260a0be 100644 --- a/src/lib/common/util/Format.ts +++ b/src/lib/common/util/Format.ts @@ -1,107 +1,112 @@ import { type CodeBlockLang } from '#lib'; -import { EscapeMarkdownOptions, Formatters, Util } from 'discord.js'; +import { + escapeBold, + escapeCodeBlock, + escapeInlineCode, + escapeItalic, + EscapeMarkdownOptions, + escapeSpoiler, + escapeStrikethrough, + escapeUnderline, + Formatters +} from 'discord.js'; /** - * Formats and escapes content for formatting + * Wraps the content inside a codeblock with no language. + * @param content The content to wrap. */ -export class Format { - /** - * Wraps the content inside a codeblock with no language. - * @param content The content to wrap. - */ - public static codeBlock(content: string): string; +export function codeBlock(content: string): string; - /** - * Wraps the content inside a codeblock with the specified language. - * @param language The language for the codeblock. - * @param content The content to wrap. - */ - public static codeBlock(language: CodeBlockLang, content: string): string; - public static codeBlock(languageOrContent: string, content?: string): string { - return typeof content === 'undefined' - ? Formatters.codeBlock(Util.escapeCodeBlock(`${languageOrContent}`)) - : Formatters.codeBlock(`${languageOrContent}`, Util.escapeCodeBlock(`${content}`)); - } +/** + * Wraps the content inside a codeblock with the specified language. + * @param language The language for the codeblock. + * @param content The content to wrap. + */ +export function codeBlock(language: CodeBlockLang, content: string): string; +export function codeBlock(languageOrContent: string, content?: string): string { + return typeof content === 'undefined' + ? Formatters.codeBlock(escapeCodeBlock(`${languageOrContent}`)) + : Formatters.codeBlock(`${languageOrContent}`, escapeCodeBlock(`${content}`)); +} - /** - * Wraps the content inside \`backticks\`, which formats it as inline code. - * @param content The content to wrap. - */ - public static inlineCode(content: string): string { - return Formatters.inlineCode(Util.escapeInlineCode(`${content}`)); - } +/** + * Wraps the content inside \`backticks\`, which formats it as inline code. + * @param content The content to wrap. + */ +export function inlineCode(content: string): string { + return Formatters.inlineCode(escapeInlineCode(`${content}`)); +} - /** - * Formats the content into italic text. - * @param content The content to wrap. - */ - public static italic(content: string): string { - return Formatters.italic(Util.escapeItalic(`${content}`)); - } +/** + * Formats the content into italic text. + * @param content The content to wrap. + */ +export function italic(content: string): string { + return Formatters.italic(escapeItalic(`${content}`)); +} - /** - * Formats the content into bold text. - * @param content The content to wrap. - */ - public static bold(content: string): string { - return Formatters.bold(Util.escapeBold(`${content}`)); - } +/** + * Formats the content into bold text. + * @param content The content to wrap. + */ +export function bold(content: string): string { + return Formatters.bold(escapeBold(`${content}`)); +} - /** - * Formats the content into underscored text. - * @param content The content to wrap. - */ - public static underscore(content: string): string { - return Formatters.underscore(Util.escapeUnderline(`${content}`)); - } +/** + * Formats the content into underscored text. + * @param content The content to wrap. + */ +export function underscore(content: string): string { + return Formatters.underscore(escapeUnderline(`${content}`)); +} - /** - * Formats the content into strike-through text. - * @param content The content to wrap. - */ - public static strikethrough(content: string): string { - return Formatters.strikethrough(Util.escapeStrikethrough(`${content}`)); - } +/** + * Formats the content into strike-through text. + * @param content The content to wrap. + */ +export function strikethrough(content: string): string { + return Formatters.strikethrough(escapeStrikethrough(`${content}`)); +} - /** - * Wraps the content inside spoiler (hidden text). - * @param content The content to wrap. - */ - public static spoiler(content: string): string { - return Formatters.spoiler(Util.escapeSpoiler(`${content}`)); - } +/** + * Wraps the content inside spoiler (hidden text). + * @param content The content to wrap. + */ +export function spoiler(content: string): string { + return Formatters.spoiler(escapeSpoiler(`${content}`)); +} - /** - * Escapes any Discord-flavour markdown in a string. - * @param text Content to escape - * @param options Options for escaping the markdown - */ - public static escapeMarkdown(text: string, options?: EscapeMarkdownOptions): string { - return Util.escapeMarkdown(`${text}`, options); - } +/** + * Escapes any Discord-flavour markdown in a string. + * @param text Content to escape + * @param options Options for escaping the markdown + */ +export function escapeMarkdown(text: string, options?: EscapeMarkdownOptions): string { + return escapeMarkdown(`${text}`, options); +} - /** - * Formats input: makes it bold and escapes any other markdown - * @param text The input - */ - public static input(text: string): string { - return this.bold(this.escapeMarkdown(this.sanitizeWtlAndControl(`${text}`))); - } +/** + * Formats input: makes it bold and escapes any other markdown + * @param text The input + */ +export function input(text: string): string { + return bold(escapeMarkdown(sanitizeWtlAndControl(`${text}`))); +} - /** - * Formats input for logs: makes it highlighted - * @param text The input - */ - public static inputLog(text: string): string { - return `<<${this.sanitizeWtlAndControl(`${text}`)}>>`; - } +/** + * Formats input for logs: makes it highlighted + * @param text The input + */ +export function inputLog(text: string): string { + return `<<${sanitizeWtlAndControl(`${text}`)}>>`; +} - /** - * Removes all characters in a string that are either control characters or change the direction of text etc. - * @param str The string you would like sanitized - */ - public static sanitizeWtlAndControl(str: string) { - // eslint-disable-next-line no-control-regex - return `${str}`.replace(/[\u0000-\u001F\u007F-\u009F\u200B]/g, ''); - } +/** + * Removes all characters in a string that are either control characters or change the direction of text etc. + * @param str The string you would like sanitized + */ +export function sanitizeWtlAndControl(str: string) { + // eslint-disable-next-line no-control-regex + return `${str}`.replace(/[\u0000-\u001F\u007F-\u009F\u200B]/g, ''); } diff --git a/src/lib/common/util/Moderation.ts b/src/lib/common/util/Moderation.ts index 6cdc141..a08dfa4 100644 --- a/src/lib/common/util/Moderation.ts +++ b/src/lib/common/util/Moderation.ts @@ -1,4 +1,16 @@ -import { ActivePunishment, ActivePunishmentType, Guild as GuildDB, ModLog, type ModLogType } from '#lib'; +import { + ActivePunishment, + ActivePunishmentType, + colors, + emojis, + format, + Guild as GuildDB, + handleError, + humanizeDuration, + ModLog, + resolveNonCachedUser, + type ModLogType +} from '#lib'; import assert from 'assert'; import { ActionRowBuilder, @@ -40,275 +52,270 @@ enum reversedPunishMap { } /** - * A utility class with moderation-related methods. + * Checks if a moderator can perform a moderation action on another user. + * @param moderator The person trying to perform the action. + * @param victim The person getting punished. + * @param type The type of punishment - used to format the response. + * @param checkModerator Whether or not to check if the victim is a moderator. + * @param force Override permissions checks. + * @returns `true` if the moderator can perform the action otherwise a reason why they can't. */ -export class Moderation { - /** - * Checks if a moderator can perform a moderation action on another user. - * @param moderator The person trying to perform the action. - * @param victim The person getting punished. - * @param type The type of punishment - used to format the response. - * @param checkModerator Whether or not to check if the victim is a moderator. - * @param force Override permissions checks. - * @returns `true` if the moderator can perform the action otherwise a reason why they can't. - */ - public static async permissionCheck( - moderator: GuildMember, - victim: GuildMember, - type: - | 'mute' - | 'unmute' - | 'warn' - | 'kick' - | 'ban' - | 'unban' - | 'add a punishment role to' - | 'remove a punishment role from' - | 'block' - | 'unblock' - | 'timeout' - | 'untimeout', - checkModerator = true, - force = false - ): Promise<true | string> { - if (force) return true; - - // If the victim is not in the guild anymore it will be undefined - if ((!victim || !victim.guild) && !['ban', 'unban'].includes(type)) return true; - - if (moderator.guild.id !== victim.guild.id) { - throw new Error('moderator and victim not in same guild'); - } - - const isOwner = moderator.guild.ownerId === moderator.id; - if (moderator.id === victim.id && !type.startsWith('un')) { - return `${util.emojis.error} You cannot ${type} yourself.`; - } - if ( - moderator.roles.highest.position <= victim.roles.highest.position && - !isOwner && - !(type.startsWith('un') && moderator.id === victim.id) - ) { - return `${util.emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as you do.`; - } - if ( - victim.roles.highest.position >= victim.guild.members.me!.roles.highest.position && - !(type.startsWith('un') && moderator.id === victim.id) - ) { - return `${util.emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as I do.`; - } - if ( - checkModerator && - victim.permissions.has(PermissionFlagsBits.ManageMessages) && - !(type.startsWith('un') && moderator.id === victim.id) - ) { - if (await moderator.guild.hasFeature('modsCanPunishMods')) { - return true; - } else { - return `${util.emojis.error} You cannot ${type} **${victim.user.tag}** because they are a moderator.`; - } - } - return true; +export async function permissionCheck( + moderator: GuildMember, + victim: GuildMember, + type: + | 'mute' + | 'unmute' + | 'warn' + | 'kick' + | 'ban' + | 'unban' + | 'add a punishment role to' + | 'remove a punishment role from' + | 'block' + | 'unblock' + | 'timeout' + | 'untimeout', + checkModerator = true, + force = false +): Promise<true | string> { + if (force) return true; + + // If the victim is not in the guild anymore it will be undefined + if ((!victim || !victim.guild) && !['ban', 'unban'].includes(type)) return true; + + if (moderator.guild.id !== victim.guild.id) { + throw new Error('moderator and victim not in same guild'); } - /** - * Creates a modlog entry for a punishment. - * @param options Options for creating a modlog entry. - * @param getCaseNumber Whether or not to get the case number of the entry. - * @returns An object with the modlog and the case number. - */ - public static async createModLogEntry( - options: CreateModLogEntryOptions, - getCaseNumber = false - ): Promise<{ log: ModLog | null; caseNum: number | null }> { - const user = (await util.resolveNonCachedUser(options.user))!.id; - const moderator = (await util.resolveNonCachedUser(options.moderator))!.id; - const guild = client.guilds.resolveId(options.guild)!; - - return this.createModLogEntrySimple( - { - ...options, - user: user, - moderator: moderator, - guild: guild - }, - getCaseNumber - ); + const isOwner = moderator.guild.ownerId === moderator.id; + if (moderator.id === victim.id && !type.startsWith('un')) { + return `${emojis.error} You cannot ${type} yourself.`; } - - /** - * Creates a modlog entry with already resolved ids. - * @param options Options for creating a modlog entry. - * @param getCaseNumber Whether or not to get the case number of the entry. - * @returns An object with the modlog and the case number. - */ - public static async createModLogEntrySimple( - options: SimpleCreateModLogEntryOptions, - getCaseNumber = false - ): Promise<{ log: ModLog | null; caseNum: number | null }> { - // If guild does not exist create it so the modlog can reference a guild. - await GuildDB.findOrCreate({ - where: { id: options.guild }, - defaults: { id: options.guild } - }); - - const modLogEntry = ModLog.build({ - type: options.type, - user: options.user, - moderator: options.moderator, - reason: options.reason, - duration: options.duration ? options.duration : undefined, - guild: options.guild, - pseudo: options.pseudo ?? false, - evidence: options.evidence, - hidden: options.hidden ?? false - }); - const saveResult: ModLog | null = await modLogEntry.save().catch(async (e) => { - await util.handleError('createModLogEntry', e); - return null; - }); - - if (!getCaseNumber) return { log: saveResult, caseNum: null }; - - const caseNum = ( - await ModLog.findAll({ where: { type: options.type, user: options.user, guild: options.guild, hidden: false } }) - )?.length; - return { log: saveResult, caseNum }; + if ( + moderator.roles.highest.position <= victim.roles.highest.position && + !isOwner && + !(type.startsWith('un') && moderator.id === victim.id) + ) { + return `${emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as you do.`; } - - /** - * Creates a punishment entry. - * @param options Options for creating the punishment entry. - * @returns The database entry, or null if no entry is created. - */ - public static async createPunishmentEntry(options: CreatePunishmentEntryOptions): Promise<ActivePunishment | null> { - const expires = options.duration ? new Date(+new Date() + options.duration ?? 0) : undefined; - const user = (await util.resolveNonCachedUser(options.user))!.id; - const guild = client.guilds.resolveId(options.guild)!; - const type = this.findTypeEnum(options.type)!; - - const entry = ActivePunishment.build( - options.extraInfo - ? { user, type, guild, expires, modlog: options.modlog, extraInfo: options.extraInfo } - : { user, type, guild, expires, modlog: options.modlog } - ); - return await entry.save().catch(async (e) => { - await util.handleError('createPunishmentEntry', e); - return null; - }); + if ( + victim.roles.highest.position >= victim.guild.members.me!.roles.highest.position && + !(type.startsWith('un') && moderator.id === victim.id) + ) { + return `${emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as I do.`; } - - /** - * Destroys a punishment entry. - * @param options Options for destroying the punishment entry. - * @returns Whether or not the entry was destroyed. - */ - public static async removePunishmentEntry(options: RemovePunishmentEntryOptions): Promise<boolean> { - const user = await util.resolveNonCachedUser(options.user); - const guild = client.guilds.resolveId(options.guild); - const type = this.findTypeEnum(options.type); - - if (!user || !guild) return false; - - let success = true; - - const entries = await ActivePunishment.findAll({ - // finding all cases of a certain type incase there were duplicates or something - where: options.extraInfo - ? { user: user.id, guild: guild, type, extraInfo: options.extraInfo } - : { user: user.id, guild: guild, type } - }).catch(async (e) => { - await util.handleError('removePunishmentEntry', e); - success = false; - }); - if (entries) { - const promises = entries.map(async (entry) => - entry.destroy().catch(async (e) => { - await util.handleError('removePunishmentEntry', e); - success = false; - }) - ); - - await Promise.all(promises); + if ( + checkModerator && + victim.permissions.has(PermissionFlagsBits.ManageMessages) && + !(type.startsWith('un') && moderator.id === victim.id) + ) { + if (await moderator.guild.hasFeature('modsCanPunishMods')) { + return true; + } else { + return `${emojis.error} You cannot ${type} **${victim.user.tag}** because they are a moderator.`; } - return success; } + return true; +} - /** - * Returns the punishment type enum for the given type. - * @param type The type of the punishment. - * @returns The punishment type enum. - */ - private static findTypeEnum(type: 'mute' | 'ban' | 'role' | 'block') { - const typeMap = { - ['mute']: ActivePunishmentType.MUTE, - ['ban']: ActivePunishmentType.BAN, - ['role']: ActivePunishmentType.ROLE, - ['block']: ActivePunishmentType.BLOCK - }; - return typeMap[type]; - } +/** + * Creates a modlog entry for a punishment. + * @param options Options for creating a modlog entry. + * @param getCaseNumber Whether or not to get the case number of the entry. + * @returns An object with the modlog and the case number. + */ +export async function createModLogEntry( + options: CreateModLogEntryOptions, + getCaseNumber = false +): Promise<{ log: ModLog | null; caseNum: number | null }> { + const user = (await resolveNonCachedUser(options.user))!.id; + const moderator = (await resolveNonCachedUser(options.moderator))!.id; + const guild = client.guilds.resolveId(options.guild)!; + + return createModLogEntrySimple( + { + ...options, + user: user, + moderator: moderator, + guild: guild + }, + getCaseNumber + ); +} - public static punishmentToPresentTense(punishment: PunishmentTypeDM): PunishmentTypePresent { - return punishMap[punishment]; - } +/** + * Creates a modlog entry with already resolved ids. + * @param options Options for creating a modlog entry. + * @param getCaseNumber Whether or not to get the case number of the entry. + * @returns An object with the modlog and the case number. + */ +export async function createModLogEntrySimple( + options: SimpleCreateModLogEntryOptions, + getCaseNumber = false +): Promise<{ log: ModLog | null; caseNum: number | null }> { + // If guild does not exist create it so the modlog can reference a guild. + await GuildDB.findOrCreate({ + where: { id: options.guild }, + defaults: { id: options.guild } + }); + + const modLogEntry = ModLog.build({ + type: options.type, + user: options.user, + moderator: options.moderator, + reason: options.reason, + duration: options.duration ? options.duration : undefined, + guild: options.guild, + pseudo: options.pseudo ?? false, + evidence: options.evidence, + hidden: options.hidden ?? false + }); + const saveResult: ModLog | null = await modLogEntry.save().catch(async (e) => { + await handleError('createModLogEntry', e); + return null; + }); + + if (!getCaseNumber) return { log: saveResult, caseNum: null }; + + const caseNum = ( + await ModLog.findAll({ where: { type: options.type, user: options.user, guild: options.guild, hidden: false } }) + )?.length; + return { log: saveResult, caseNum }; +} - public static punishmentToPastTense(punishment: PunishmentTypePresent): PunishmentTypeDM { - return reversedPunishMap[punishment]; - } +/** + * Creates a punishment entry. + * @param options Options for creating the punishment entry. + * @returns The database entry, or null if no entry is created. + */ +export async function createPunishmentEntry(options: CreatePunishmentEntryOptions): Promise<ActivePunishment | null> { + const expires = options.duration ? new Date(+new Date() + options.duration ?? 0) : undefined; + const user = (await resolveNonCachedUser(options.user))!.id; + const guild = client.guilds.resolveId(options.guild)!; + const type = findTypeEnum(options.type)!; + + const entry = ActivePunishment.build( + options.extraInfo + ? { user, type, guild, expires, modlog: options.modlog, extraInfo: options.extraInfo } + : { user, type, guild, expires, modlog: options.modlog } + ); + return await entry.save().catch(async (e) => { + await handleError('createPunishmentEntry', e); + return null; + }); +} - /** - * Notifies the specified user of their punishment. - * @param options Options for notifying the user. - * @returns Whether or not the dm was successfully sent. - */ - public static async punishDM(options: PunishDMOptions): Promise<boolean> { - const ending = await options.guild.getSetting('punishmentEnding'); - const dmEmbed = - ending && ending.length && options.sendFooter - ? new EmbedBuilder().setDescription(ending).setColor(util.colors.newBlurple) - : undefined; - - const appealsEnabled = !!( - (await options.guild.hasFeature('punishmentAppeals')) && (await options.guild.getLogChannel('appeals')) +/** + * Destroys a punishment entry. + * @param options Options for destroying the punishment entry. + * @returns Whether or not the entry was destroyed. + */ +export async function removePunishmentEntry(options: RemovePunishmentEntryOptions): Promise<boolean> { + const user = await resolveNonCachedUser(options.user); + const guild = client.guilds.resolveId(options.guild); + const type = findTypeEnum(options.type); + + if (!user || !guild) return false; + + let success = true; + + const entries = await ActivePunishment.findAll({ + // finding all cases of a certain type incase there were duplicates or something + where: options.extraInfo + ? { user: user.id, guild: guild, type, extraInfo: options.extraInfo } + : { user: user.id, guild: guild, type } + }).catch(async (e) => { + await handleError('removePunishmentEntry', e); + success = false; + }); + if (entries) { + const promises = entries.map(async (entry) => + entry.destroy().catch(async (e) => { + await handleError('removePunishmentEntry', e); + success = false; + }) ); - let content = `You have been ${options.punishment} `; - if (options.punishment.includes('blocked')) { - assert(options.channel); - content += `from <#${options.channel}> `; - } - content += `in ${util.format.input(options.guild.name)} `; - if (options.duration !== null && options.duration !== undefined) - content += options.duration ? `for ${util.humanizeDuration(options.duration)} ` : 'permanently '; - const reason = options.reason?.trim() ? options.reason?.trim() : 'No reason provided'; - content += `for ${util.format.input(reason)}.`; - - let components; - if (appealsEnabled && options.modlog) - components = [ - new ActionRowBuilder<ButtonBuilder>({ - components: [ - new ButtonBuilder({ - customId: `appeal;${this.punishmentToPresentTense(options.punishment)};${options.guild.id};${client.users.resolveId( - options.user - )};${options.modlog}`, - style: ButtonStyle.Primary, - label: 'Appeal' - }).toJSON() - ] - }) - ]; - - const dmSuccess = await client.users - .send(options.user, { - content, - embeds: dmEmbed ? [dmEmbed] : undefined, - components - }) - .catch(() => false); - return !!dmSuccess; + await Promise.all(promises); } + return success; +} + +/** + * Returns the punishment type enum for the given type. + * @param type The type of the punishment. + * @returns The punishment type enum. + */ +function findTypeEnum(type: 'mute' | 'ban' | 'role' | 'block') { + const typeMap = { + ['mute']: ActivePunishmentType.MUTE, + ['ban']: ActivePunishmentType.BAN, + ['role']: ActivePunishmentType.ROLE, + ['block']: ActivePunishmentType.BLOCK + }; + return typeMap[type]; +} + +export function punishmentToPresentTense(punishment: PunishmentTypeDM): PunishmentTypePresent { + return punishMap[punishment]; +} + +export function punishmentToPastTense(punishment: PunishmentTypePresent): PunishmentTypeDM { + return reversedPunishMap[punishment]; +} + +/** + * Notifies the specified user of their punishment. + * @param options Options for notifying the user. + * @returns Whether or not the dm was successfully sent. + */ +export async function punishDM(options: PunishDMOptions): Promise<boolean> { + const ending = await options.guild.getSetting('punishmentEnding'); + const dmEmbed = + ending && ending.length && options.sendFooter + ? new EmbedBuilder().setDescription(ending).setColor(colors.newBlurple) + : undefined; + + const appealsEnabled = !!( + (await options.guild.hasFeature('punishmentAppeals')) && (await options.guild.getLogChannel('appeals')) + ); + + let content = `You have been ${options.punishment} `; + if (options.punishment.includes('blocked')) { + assert(options.channel); + content += `from <#${options.channel}> `; + } + content += `in ${format.input(options.guild.name)} `; + if (options.duration !== null && options.duration !== undefined) + content += options.duration ? `for ${humanizeDuration(options.duration)} ` : 'permanently '; + const reason = options.reason?.trim() ? options.reason?.trim() : 'No reason provided'; + content += `for ${format.input(reason)}.`; + + let components; + if (appealsEnabled && options.modlog) + components = [ + new ActionRowBuilder<ButtonBuilder>({ + components: [ + new ButtonBuilder({ + customId: `appeal;${punishmentToPresentTense(options.punishment)};${options.guild.id};${client.users.resolveId( + options.user + )};${options.modlog}`, + style: ButtonStyle.Primary, + label: 'Appeal' + }).toJSON() + ] + }) + ]; + + const dmSuccess = await client.users + .send(options.user, { + content, + embeds: dmEmbed ? [dmEmbed] : undefined, + components + }) + .catch(() => false); + return !!dmSuccess; } interface BaseCreateModLogEntryOptions { |