diff options
author | IRONM00N <64110067+IRONM00N@users.noreply.github.com> | 2021-10-12 20:27:37 -0400 |
---|---|---|
committer | IRONM00N <64110067+IRONM00N@users.noreply.github.com> | 2021-10-12 20:27:37 -0400 |
commit | ba2d7b7db0a627234ed08de9d6bec8cb675404a7 (patch) | |
tree | 9ade9ed549b52eac3f2966a5cee5478267eca7c4 /src/lib/extensions | |
parent | cac6abf3efd563b83f8f0ce70ce4bcfa5ada1a27 (diff) | |
download | tanzanite-ba2d7b7db0a627234ed08de9d6bec8cb675404a7.tar.gz tanzanite-ba2d7b7db0a627234ed08de9d6bec8cb675404a7.tar.bz2 tanzanite-ba2d7b7db0a627234ed08de9d6bec8cb675404a7.zip |
revamp automod, refactoring, fixes
Diffstat (limited to 'src/lib/extensions')
-rw-r--r-- | src/lib/extensions/discord-akairo/BushClientUtil.ts | 361 | ||||
-rw-r--r-- | src/lib/extensions/discord.js/BushGuild.ts | 22 | ||||
-rw-r--r-- | src/lib/extensions/discord.js/BushGuildMember.ts | 25 |
3 files changed, 123 insertions, 285 deletions
diff --git a/src/lib/extensions/discord-akairo/BushClientUtil.ts b/src/lib/extensions/discord-akairo/BushClientUtil.ts index a50cd61..448eaf3 100644 --- a/src/lib/extensions/discord-akairo/BushClientUtil.ts +++ b/src/lib/extensions/discord-akairo/BushClientUtil.ts @@ -3,16 +3,10 @@ import { BushCache, BushClient, BushConstants, - BushGuildMember, - BushGuildMemberResolvable, - BushGuildResolvable, BushMessage, BushSlashMessage, BushUser, Global, - Guild, - ModLog, - ModLogType, Pronoun, PronounCode } from '@lib'; @@ -54,7 +48,6 @@ import _ from 'lodash'; import moment from 'moment'; import { inspect, InspectOptions, promisify } from 'util'; import CommandErrorListener from '../../../listeners/commands/commandError'; -import { ActivePunishment, ActivePunishmentType } from '../../models/ActivePunishment'; import { BushNewsChannel } from '../discord.js/BushNewsChannel'; import { BushTextChannel } from '../discord.js/BushTextChannel'; import { BushSlashEditMessageType, BushSlashSendMessageType, BushUserResolvable } from './BushClient'; @@ -1079,174 +1072,6 @@ export class BushClientUtil extends ClientUtil { return { duration, contentWithoutTime }; } - /** - * Checks if a moderator can perform a moderation action on another user. - * @param moderator - The person trying to perform the action. - * @param victim - The person getting punished. - * @param type - The type of punishment - used to format the response. - * @param checkModerator - Whether or not to check if the victim is a moderator. - */ - public async moderationPermissionCheck( - moderator: BushGuildMember, - victim: BushGuildMember, - type: 'mute' | 'unmute' | 'warn' | 'kick' | 'ban' | 'unban' | 'add a punishment role to' | 'remove a punishment role from', - checkModerator = true, - force = false - ): Promise<true | string> { - if (force) return true; - - // If the victim is not in the guild anymore it will be undefined - if ((!victim || !victim.guild) && !['ban', 'unban'].includes(type)) return true; - - if (moderator.guild.id !== victim.guild.id) { - throw new Error('moderator and victim not in same guild'); - } - - const isOwner = moderator.guild.ownerId === moderator.id; - if (moderator.id === victim.id && !type.startsWith('un')) { - return `${util.emojis.error} You cannot ${type} yourself.`; - } - if ( - moderator.roles.highest.position <= victim.roles.highest.position && - !isOwner && - !(type.startsWith('un') && moderator.id === victim.id) - ) { - return `${util.emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as you do.`; - } - if ( - victim.roles.highest.position >= victim.guild.me!.roles.highest.position && - !(type.startsWith('un') && moderator.id === victim.id) - ) { - return `${util.emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as I do.`; - } - if (checkModerator && victim.permissions.has('MANAGE_MESSAGES') && !(type.startsWith('un') && moderator.id === victim.id)) { - if (await moderator.guild.hasFeature('modsCanPunishMods')) { - return true; - } else { - return `${util.emojis.error} You cannot ${type} **${victim.user.tag}** because they are a moderator.`; - } - } - return true; - } - - public async createModLogEntry( - options: { - type: ModLogType; - user: BushGuildMemberResolvable; - moderator: BushGuildMemberResolvable; - reason: string | undefined | null; - duration?: number; - guild: BushGuildResolvable; - pseudo?: boolean; - }, - getCaseNumber = false - ): Promise<{ log: ModLog | null; caseNum: number | null }> { - const user = (await util.resolveNonCachedUser(options.user))!.id; - const moderator = (await util.resolveNonCachedUser(options.moderator))!.id; - const guild = client.guilds.resolveId(options.guild)!; - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const duration = options.duration || undefined; - - // If guild does not exist create it so the modlog can reference a guild. - await Guild.findOrCreate({ - where: { - id: guild - }, - defaults: { - id: guild - } - }); - - const modLogEntry = ModLog.build({ - type: options.type, - user, - moderator, - reason: options.reason, - duration: duration, - guild, - pseudo: options.pseudo ?? false - }); - const saveResult: ModLog | null = await modLogEntry.save().catch(async (e) => { - await util.handleError('createModLogEntry', e); - return null; - }); - - if (!getCaseNumber) return { log: saveResult, caseNum: null }; - - const caseNum = (await ModLog.findAll({ where: { type: options.type, user: user, guild: guild, hidden: 'false' } })) - ?.length; - return { log: saveResult, caseNum }; - } - - public async createPunishmentEntry(options: { - type: 'mute' | 'ban' | 'role' | 'block'; - user: BushGuildMemberResolvable; - duration: number | undefined; - guild: BushGuildResolvable; - modlog: string; - extraInfo?: Snowflake; - }): Promise<ActivePunishment | null> { - const expires = options.duration ? new Date(+new Date() + options.duration ?? 0) : undefined; - const user = (await util.resolveNonCachedUser(options.user))!.id; - const guild = client.guilds.resolveId(options.guild)!; - const type = this.#findTypeEnum(options.type)!; - - const entry = ActivePunishment.build( - options.extraInfo - ? { user, type, guild, expires, modlog: options.modlog, extraInfo: options.extraInfo } - : { user, type, guild, expires, modlog: options.modlog } - ); - return await entry.save().catch(async (e) => { - await util.handleError('createPunishmentEntry', e); - return null; - }); - } - - public async removePunishmentEntry(options: { - type: 'mute' | 'ban' | 'role' | 'block'; - user: BushGuildMemberResolvable; - guild: BushGuildResolvable; - extraInfo?: Snowflake; - }): Promise<boolean> { - const user = await util.resolveNonCachedUser(options.user); - const guild = client.guilds.resolveId(options.guild); - const type = this.#findTypeEnum(options.type); - - if (!user || !guild) return false; - - let success = true; - - const entries = await ActivePunishment.findAll({ - // finding all cases of a certain type incase there were duplicates or something - where: options.extraInfo - ? { user: user.id, guild: guild, type, extraInfo: options.extraInfo } - : { user: user.id, guild: guild, type } - }).catch(async (e) => { - await util.handleError('removePunishmentEntry', e); - success = false; - }); - if (entries) { - // eslint-disable-next-line @typescript-eslint/no-misused-promises - entries.forEach(async (entry) => { - await entry.destroy().catch(async (e) => { - await util.handleError('removePunishmentEntry', e); - }); - success = false; - }); - } - return success; - } - - #findTypeEnum(type: 'mute' | 'ban' | 'role' | 'block') { - const typeMap = { - ['mute']: ActivePunishmentType.MUTE, - ['ban']: ActivePunishmentType.BAN, - ['role']: ActivePunishmentType.ROLE, - ['block']: ActivePunishmentType.BLOCK - }; - return typeMap[type]; - } - public humanizeDuration(duration: number, largest?: number): string { if (largest) return humanizeDuration(duration, { language: 'en', maxDecimalPoints: 2, largest }); else return humanizeDuration(duration, { language: 'en', maxDecimalPoints: 2 }); @@ -1315,6 +1140,99 @@ export class BushClientUtil extends ClientUtil { return string.charAt(0)?.toUpperCase() + string.slice(1); } + /** + * Wait an amount in seconds. + */ + public async sleep(s: number): Promise<unknown> { + return new Promise((resolve) => setTimeout(resolve, s * 1000)); + } + + public async handleError(context: string, error: Error) { + await client.console.error(_.camelCase(context), `An error occurred:\n${error?.stack ?? (error as any)}`, false); + await client.console.channelError({ + embeds: [await CommandErrorListener.generateErrorEmbed({ type: 'unhandledRejection', error: error, context })] + }); + } + + public async resolveNonCachedUser(user: UserResolvable | undefined | null): Promise<BushUser | undefined> { + if (!user) return undefined; + const id = + user instanceof User || user instanceof GuildMember || user instanceof ThreadMember + ? user.id + : user instanceof Message + ? user.author.id + : typeof user === 'string' + ? user + : undefined; + if (!id) return undefined; + else return await client.users.fetch(id).catch(() => undefined); + } + + public async getPronounsOf(user: User | Snowflake): Promise<Pronoun | undefined> { + const _user = await this.resolveNonCachedUser(user); + if (!_user) throw new Error(`Cannot find user ${user}`); + const apiRes = (await got + .get(`https://pronoundb.org/api/v1/lookup?platform=discord&id=${_user.id}`) + .json() + .catch(() => undefined)) as { pronouns: PronounCode } | undefined; + + if (!apiRes) return undefined; + if (!apiRes.pronouns) throw new Error('apiRes.pronouns is undefined'); + + return client.constants.pronounMapping[apiRes.pronouns]; + } + + // modified from https://stackoverflow.com/questions/31054910/get-functions-methods-of-a-class + // answer by Bruno Grieder + public getMethods(_obj: any): string { + let props: string[] = []; + let obj: any = new Object(_obj); + + do { + const l = Object.getOwnPropertyNames(obj) + .concat(Object.getOwnPropertySymbols(obj).map((s) => s.toString())) + .sort() + .filter( + (p, i, arr) => + typeof Object.getOwnPropertyDescriptor(obj, p)?.['get'] !== 'function' && // ignore getters + typeof Object.getOwnPropertyDescriptor(obj, p)?.['set'] !== 'function' && // ignore setters + typeof obj[p] === 'function' && //only the methods + p !== 'constructor' && //not the constructor + (i == 0 || p !== arr[i - 1]) && //not overriding in this prototype + props.indexOf(p) === -1 //not overridden in a child + ); + + const reg = /\(([\s\S]*?)\)/; + props = props.concat( + l.map( + (p) => + `${obj[p] && obj[p][Symbol.toStringTag] === 'AsyncFunction' ? 'async ' : ''}function ${p}(${ + reg.exec(obj[p].toString())?.[1] + ? reg + .exec(obj[p].toString())?.[1] + .split(', ') + .map((arg) => arg.split('=')[0].trim()) + .join(', ') + : '' + });` + ) + ); + } while ( + (obj = Object.getPrototypeOf(obj)) && //walk-up the prototype chain + Object.getPrototypeOf(obj) //not the the Object prototype methods (hasOwnProperty, etc...) + ); + + return props.join('\n'); + } + + /** + * Removes all characters in a string that are either control characters or change the direction of text etc. + */ + public sanitizeWtlAndControl(str: string) { + // eslint-disable-next-line no-control-regex + return str.replace(/[\u0000-\u001F\u007F-\u009F\u200B]/g, ''); + } + get arg() { return class Arg { /** @@ -1435,99 +1353,6 @@ export class BushClientUtil extends ClientUtil { } /** - * Wait an amount in seconds. - */ - public async sleep(s: number): Promise<unknown> { - return new Promise((resolve) => setTimeout(resolve, s * 1000)); - } - - public async handleError(context: string, error: Error) { - await client.console.error(_.camelCase(context), `An error occurred:\n${error?.stack ?? (error as any)}`, false); - await client.console.channelError({ - embeds: [await CommandErrorListener.generateErrorEmbed({ type: 'unhandledRejection', error: error, context })] - }); - } - - public async resolveNonCachedUser(user: UserResolvable | undefined | null): Promise<BushUser | undefined> { - if (!user) return undefined; - const id = - user instanceof User || user instanceof GuildMember || user instanceof ThreadMember - ? user.id - : user instanceof Message - ? user.author.id - : typeof user === 'string' - ? user - : undefined; - if (!id) return undefined; - else return await client.users.fetch(id).catch(() => undefined); - } - - public async getPronounsOf(user: User | Snowflake): Promise<Pronoun | undefined> { - const _user = await this.resolveNonCachedUser(user); - if (!_user) throw new Error(`Cannot find user ${user}`); - const apiRes = (await got - .get(`https://pronoundb.org/api/v1/lookup?platform=discord&id=${_user.id}`) - .json() - .catch(() => undefined)) as { pronouns: PronounCode } | undefined; - - if (!apiRes) return undefined; - if (!apiRes.pronouns) throw new Error('apiRes.pronouns is undefined'); - - return client.constants.pronounMapping[apiRes.pronouns]; - } - - // modified from https://stackoverflow.com/questions/31054910/get-functions-methods-of-a-class - // answer by Bruno Grieder - public getMethods(_obj: any): string { - let props: string[] = []; - let obj: any = new Object(_obj); - - do { - const l = Object.getOwnPropertyNames(obj) - .concat(Object.getOwnPropertySymbols(obj).map((s) => s.toString())) - .sort() - .filter( - (p, i, arr) => - typeof Object.getOwnPropertyDescriptor(obj, p)?.['get'] !== 'function' && // ignore getters - typeof Object.getOwnPropertyDescriptor(obj, p)?.['set'] !== 'function' && // ignore setters - typeof obj[p] === 'function' && //only the methods - p !== 'constructor' && //not the constructor - (i == 0 || p !== arr[i - 1]) && //not overriding in this prototype - props.indexOf(p) === -1 //not overridden in a child - ); - - const reg = /\(([\s\S]*?)\)/; - props = props.concat( - l.map( - (p) => - `${obj[p] && obj[p][Symbol.toStringTag] === 'AsyncFunction' ? 'async ' : ''}function ${p}(${ - reg.exec(obj[p].toString())?.[1] - ? reg - .exec(obj[p].toString())?.[1] - .split(', ') - .map((arg) => arg.split('=')[0].trim()) - .join(', ') - : '' - });` - ) - ); - } while ( - (obj = Object.getPrototypeOf(obj)) && //walk-up the prototype chain - Object.getPrototypeOf(obj) //not the the Object prototype methods (hasOwnProperty, etc...) - ); - - return props.join('\n'); - } - - /** - * Removes all characters in a string that are either control characters or change the direction of text etc. - */ - public sanitizeWtlAndControl(str: string) { - // eslint-disable-next-line no-control-regex - return str.replace(/[\u0000-\u001F\u007F-\u009F\u200B]/g, ''); - } - - /** * Discord.js's Util class */ get discord() { diff --git a/src/lib/extensions/discord.js/BushGuild.ts b/src/lib/extensions/discord.js/BushGuild.ts index 256b9dc..3a2ae51 100644 --- a/src/lib/extensions/discord.js/BushGuild.ts +++ b/src/lib/extensions/discord.js/BushGuild.ts @@ -1,5 +1,6 @@ -import { Guild, UserResolvable } from 'discord.js'; +import { Guild, MessageOptions, UserResolvable } from 'discord.js'; import { RawGuildData } from 'discord.js/typings/rawDataTypes'; +import { Moderation } from '../../common/moderation'; import { Guild as GuildDB, GuildFeatures, GuildLogType, GuildModel } from '../../models/Guild'; import { ModLogType } from '../../models/ModLog'; import { BushClient, BushUserResolvable } from '../discord-akairo/BushClient'; @@ -96,7 +97,7 @@ export class BushGuild extends Guild { if (!banSuccess) return 'error banning'; // add modlog entry - const { log: modlog } = await util.createModLogEntry({ + const { log: modlog } = await Moderation.createModLogEntry({ type: options.duration ? ModLogType.TEMP_BAN : ModLogType.PERM_BAN, user: user, moderator: moderator.id, @@ -108,7 +109,7 @@ export class BushGuild extends Guild { caseID = modlog.id; // add punishment entry so they can be unbanned later - const punishmentEntrySuccess = await util.createPunishmentEntry({ + const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ type: 'ban', user: user, guild: this, @@ -161,7 +162,7 @@ export class BushGuild extends Guild { if (!unbanSuccess) return 'error unbanning'; // add modlog entry - const { log: modlog } = await util.createModLogEntry({ + const { log: modlog } = await Moderation.createModLogEntry({ type: ModLogType.UNBAN, user: user.id, moderator: moderator.id, @@ -172,7 +173,7 @@ export class BushGuild extends Guild { caseID = modlog.id; // remove punishment entry - const removePunishmentEntrySuccess = await util.removePunishmentEntry({ + const removePunishmentEntrySuccess = await Moderation.removePunishmentEntry({ type: 'ban', user: user.id, guild: this @@ -192,4 +193,15 @@ export class BushGuild extends Guild { client.emit('bushUnban', user, moderator, this, options.reason ?? undefined, caseID!, dmSuccessEvent!); return ret; } + + /** + * Sends a message to the guild's specified logging channel. + */ + public async sendLogChannel(logType: GuildLogType, message: MessageOptions) { + const logChannel = await this.getLogChannel(logType); + if (!logChannel || logChannel.type !== 'GUILD_TEXT') return; + if (!logChannel.permissionsFor(this.me!.id)?.has(['VIEW_CHANNEL', 'SEND_MESSAGES', 'EMBED_LINKS'])) return; + + return await logChannel.send(message).catch(() => null); + } } diff --git a/src/lib/extensions/discord.js/BushGuildMember.ts b/src/lib/extensions/discord.js/BushGuildMember.ts index 8e855f7..b4c136c 100644 --- a/src/lib/extensions/discord.js/BushGuildMember.ts +++ b/src/lib/extensions/discord.js/BushGuildMember.ts @@ -1,5 +1,6 @@ import { GuildMember, MessageEmbed, Partialize, Role } from 'discord.js'; import { RawGuildMemberData } from 'discord.js/typings/rawDataTypes'; +import { Moderation } from '../../common/moderation'; import { ModLogType } from '../../models/ModLog'; import { BushClient } from '../discord-akairo/BushClient'; import { BushGuild } from './BushGuild'; @@ -111,7 +112,7 @@ export class BushGuildMember extends GuildMember { const ret = await (async () => { // add modlog entry - const result = await util.createModLogEntry( + const result = await Moderation.createModLogEntry( { type: ModLogType.WARN, user: this, @@ -145,7 +146,7 @@ export class BushGuildMember extends GuildMember { const ret = await (async () => { if (options.addToModlog || options.duration) { - const { log: modlog } = await util.createModLogEntry({ + const { log: modlog } = await Moderation.createModLogEntry({ type: options.duration ? ModLogType.TEMP_PUNISHMENT_ROLE : ModLogType.PERM_PUNISHMENT_ROLE, guild: this.guild, moderator: moderator.id, @@ -158,7 +159,7 @@ export class BushGuildMember extends GuildMember { caseID = modlog.id; if (options.addToModlog || options.duration) { - const punishmentEntrySuccess = await util.createPunishmentEntry({ + const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ type: 'role', user: this, guild: this.guild, @@ -198,7 +199,7 @@ export class BushGuildMember extends GuildMember { const ret = await (async () => { if (options.addToModlog) { - const { log: modlog } = await util.createModLogEntry({ + const { log: modlog } = await Moderation.createModLogEntry({ type: ModLogType.REMOVE_PUNISHMENT_ROLE, guild: this.guild, moderator: moderator.id, @@ -209,7 +210,7 @@ export class BushGuildMember extends GuildMember { if (!modlog) return 'error creating modlog entry'; caseID = modlog.id; - const punishmentEntrySuccess = await util.removePunishmentEntry({ + const punishmentEntrySuccess = await Moderation.removePunishmentEntry({ type: 'role', user: this, guild: this.guild, @@ -281,7 +282,7 @@ export class BushGuildMember extends GuildMember { if (!muteSuccess) return 'error giving mute role'; // add modlog entry - const { log: modlog } = await util.createModLogEntry({ + const { log: modlog } = await Moderation.createModLogEntry({ type: options.duration ? ModLogType.TEMP_MUTE : ModLogType.PERM_MUTE, user: this, moderator: moderator.id, @@ -294,7 +295,7 @@ export class BushGuildMember extends GuildMember { caseID = modlog.id; // add punishment entry so they can be unmuted later - const punishmentEntrySuccess = await util.createPunishmentEntry({ + const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ type: 'mute', user: this, guild: this.guild, @@ -351,7 +352,7 @@ export class BushGuildMember extends GuildMember { if (!muteSuccess) return 'error removing mute role'; //remove modlog entry - const { log: modlog } = await util.createModLogEntry({ + const { log: modlog } = await Moderation.createModLogEntry({ type: ModLogType.UNMUTE, user: this, moderator: moderator.id, @@ -363,7 +364,7 @@ export class BushGuildMember extends GuildMember { caseID = modlog.id; // remove mute entry - const removePunishmentEntrySuccess = await util.removePunishmentEntry({ + const removePunishmentEntrySuccess = await Moderation.removePunishmentEntry({ type: 'mute', user: this, guild: this.guild @@ -402,7 +403,7 @@ export class BushGuildMember extends GuildMember { if (!kickSuccess) return 'error kicking'; // add modlog entry - const { log: modlog } = await util.createModLogEntry({ + const { log: modlog } = await Moderation.createModLogEntry({ type: ModLogType.KICK, user: this, moderator: moderator.id, @@ -439,7 +440,7 @@ export class BushGuildMember extends GuildMember { if (!banSuccess) return 'error banning'; // add modlog entry - const { log: modlog } = await util.createModLogEntry({ + const { log: modlog } = await Moderation.createModLogEntry({ type: options.duration ? ModLogType.TEMP_BAN : ModLogType.PERM_BAN, user: this, moderator: moderator.id, @@ -451,7 +452,7 @@ export class BushGuildMember extends GuildMember { caseID = modlog.id; // add punishment entry so they can be unbanned later - const punishmentEntrySuccess = await util.createPunishmentEntry({ + const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ type: 'ban', user: this, guild: this.guild, |