diff options
31 files changed, 1035 insertions, 317 deletions
diff --git a/package.json b/package.json index 50c1762..7424bea 100644 --- a/package.json +++ b/package.json @@ -160,7 +160,17 @@ "useTabs": true, "quoteProps": "consistent", "singleQuote": true, - "trailingComma": "none" + "trailingComma": "none", + "overrides": [ + { + "files": [ + "*BushClientEvents.d.ts" + ], + "options": { + "printWidth": 80 + } + } + ] }, "packageManager": "yarn@3.0.1", "dependenciesMeta": { diff --git a/src/commands/_fake-command/ironmoon.ts b/src/commands/_fake-command/ironmoon.ts index 8ca1f5b..ddc6ced 100644 --- a/src/commands/_fake-command/ironmoon.ts +++ b/src/commands/_fake-command/ironmoon.ts @@ -5,7 +5,7 @@ export default class IronmoonCommand extends BushCommand { super('ironmoon', { category: 'fake-commands', description: { content: '', examples: '', usage: '' }, - completelyHide: true + pseudo: true }); } public override condition(message: BushMessage): boolean { diff --git a/src/commands/config/log.ts b/src/commands/config/log.ts index 592f700..0bc2189 100644 --- a/src/commands/config/log.ts +++ b/src/commands/config/log.ts @@ -79,7 +79,7 @@ export default class LogCommand extends BushCommand { ? `${util.emojis.success} Successfully ${oldChannel ? 'changed' : 'set'}` : `${util.emojis.error} Unable to ${oldChannel ? 'change' : 'set'}` } ${ - oldChannel ? ` the \`${args.log_type}\` log channel from <#${oldChannel}>` : ` the \`${args.log_type}\` log channel` + oldChannel ? ` the **${args.log_type}** log channel from <#${oldChannel}>` : ` the \`${args.log_type}\` log channel` } to ${args.channel ? `<#${args.channel.id}>` : '`disabled`'}` ); } diff --git a/src/commands/info/help.ts b/src/commands/info/help.ts index ad4e00f..1338f8a 100644 --- a/src/commands/info/help.ts +++ b/src/commands/info/help.ts @@ -90,14 +90,14 @@ export default class HelpCommand extends BushCommand { : args.command : null; if (!isOwner) args.showHidden = false; - if (!command || command.completelyHide) { + if (!command || command.pseudo) { const embed = new MessageEmbed().setColor(util.colors.default).setTimestamp(); if (message.guild) { embed.setFooter(`For more information about a command use ${prefix}help <command>`); } for (const [, category] of this.handler.categories) { const categoryFilter = category.filter((command) => { - if (command.completelyHide) return false; + if (command.pseudo) return false; if (command.hidden && !args.showHidden) return false; if (command.channel == 'guild' && !message.guild && !args.showHidden) return false; if (command.ownerOnly && !isOwner) return false; diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 5a1b5d9..812d7ca 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -126,7 +126,7 @@ export default class BanCommand extends BushCommand { duration: time! ?? 0, deleteDays: days ?? 0 }) - : await message.guild.ban({ + : await message.guild.bushBan({ user, reason: parsedReason, moderator: message.author, diff --git a/src/commands/moderation/hideCase.ts b/src/commands/moderation/hideCase.ts new file mode 100644 index 0000000..1d8dea6 --- /dev/null +++ b/src/commands/moderation/hideCase.ts @@ -0,0 +1,50 @@ +import { BushCommand, BushMessage, BushSlashMessage, ModLog } from '@lib'; + +export default class HideCaseCommand extends BushCommand { + public constructor() { + super('hideCase', { + aliases: ['hidecase', 'hide_case', 'showcase', 'show_case', 'coverupmodabuse', 'cover_up_mod_abuse'], + category: 'moderation', + description: { + content: 'Hide a particular modlog case from the modlog command unless the `--hidden` flag is specified', + usage: 'hideCase <caseID>', + examples: ['hideCase 9210b1ea-91f5-4ea2-801b-02b394469c77'] + }, + args: [ + { + id: 'case', + type: 'string', + prompt: { + start: 'What modlog case would you like to hide?', + retry: '{error} Choose a valid case id.' + } + } + ], + userPermissions: ['MANAGE_MESSAGES'], + slash: true, + slashOptions: [ + { + name: 'case', + description: 'What modlog case would you like to hide?', + type: 'STRING', + required: true + } + ], + channel: 'guild' + }); + } + + public override async exec(message: BushMessage | BushSlashMessage, { case: caseID }: { case: string }): Promise<unknown> { + if (message.author.id === '496409778822709251') + return await message.util.reply(`${util.emojis.error} This command is Bestower proof.`); + const entry = await ModLog.findByPk(caseID); + if (!entry || entry.pseudo) return message.util.send(`${util.emojis.error} Invalid entry.`); + if (entry.guild !== message.guild!.id) + return message.util.reply(`${util.emojis.error} This modlog is from another server.`); + const action = entry.hidden ? 'now hidden' : 'no longer hidden'; + entry.hidden = !entry.hidden; + await entry.save(); + + return await message.util.reply(`${util.emojis.success} CaseID \`${caseID}\` is ${action}.`); + } +} diff --git a/src/commands/moderation/modlog.ts b/src/commands/moderation/modlog.ts index fd53ea7..0be6971 100644 --- a/src/commands/moderation/modlog.ts +++ b/src/commands/moderation/modlog.ts @@ -1,5 +1,5 @@ import { BushCommand, BushMessage, BushSlashMessage, BushUser, ModLog } from '@lib'; -import { MessageEmbed, User } from 'discord.js'; +import { User } from 'discord.js'; export default class ModlogCommand extends BushCommand { public constructor() { @@ -8,7 +8,7 @@ export default class ModlogCommand extends BushCommand { category: 'moderation', description: { content: "View a user's modlogs, or view a specific case.", - usage: 'modlogs <search>', + usage: 'modlogs <search> [--hidden]', examples: ['modlogs @Tyman'] }, args: [ @@ -19,6 +19,12 @@ export default class ModlogCommand extends BushCommand { start: 'What case id or user would you like to see?', retry: '{error} Choose a valid case id or user.' } + }, + { + id: 'hidden', + match: 'flag', + flags: ['--hidden', '-h'], + default: false } ], userPermissions: ['MANAGE_MESSAGES'], @@ -29,6 +35,12 @@ export default class ModlogCommand extends BushCommand { description: 'What case id or user would you like to see?', type: 'STRING', required: true + }, + { + name: 'hidden', + description: 'Would you like to see hidden modlogs?', + type: 'BOOLEAN', + required: false } ] }); @@ -50,7 +62,7 @@ export default class ModlogCommand extends BushCommand { public override async exec( message: BushMessage | BushSlashMessage, - { search }: { search: BushUser | string } + { search, hidden }: { search: BushUser | string; hidden: boolean } ): Promise<unknown> { const foundUser = search instanceof User ? search : await util.resolveUserAsync(search); if (foundUser) { @@ -62,28 +74,25 @@ export default class ModlogCommand extends BushCommand { order: [['createdAt', 'ASC']] }); if (!logs.length) return message.util.reply(`${util.emojis.error} **${foundUser.tag}** does not have any modlogs.`); - const niceLogs: string[] = []; - for (const log of logs) { - niceLogs.push(this.#generateModlogInfo(log)); - } + const niceLogs = logs.filter((log) => !log.pseudo && !log.hidden && !hidden).map((log) => this.#generateModlogInfo(log)); const chunked: string[][] = util.chunk(niceLogs, 3); - const embedPages = chunked.map( - (chunk) => - new MessageEmbed({ - title: `${foundUser.tag}'s Mod Logs`, - description: chunk.join('\n━━━━━━━━━━━━━━━\n'), - color: util.colors.default - }) - ); + const embedPages = chunked.map((chunk) => ({ + title: `${foundUser.tag}'s Mod Logs`, + description: chunk.join('\n━━━━━━━━━━━━━━━\n'), + color: util.colors.default + })); return await util.buttonPaginate(message, embedPages, undefined, true); } else if (search) { const entry = await ModLog.findByPk(search as string); - if (!entry) return message.util.send(`${util.emojis.error} That modlog does not exist.`); - const embed = new MessageEmbed({ + if (!entry || entry.pseudo || (entry.hidden && !hidden)) + return message.util.send(`${util.emojis.error} That modlog does not exist.`); + if (entry.guild !== message.guild!.id) + return message.util.reply(`${util.emojis.error} This modlog is from another server.`); + const embed = { title: `Case ${entry.id}`, description: this.#generateModlogInfo(entry), color: util.colors.default - }); + }; return await util.buttonPaginate(message, [embed]); } } diff --git a/src/commands/moderation/purge.ts b/src/commands/moderation/purge.ts index b391ff6..4ed1ee7 100644 --- a/src/commands/moderation/purge.ts +++ b/src/commands/moderation/purge.ts @@ -47,18 +47,19 @@ export default class PurgeCommand extends BushCommand { if (message.channel.type === 'DM') return message.util.reply(`${util.emojis.error} You cannot run this command in dms.`); if (args.amount > 100 || args.amount < 1) return message.util.reply(`${util.emojis.error} `); - const messages = (await message.channel.messages.fetch({ limit: args.amount })).filter((message) => filter(message)); - const filter = (filterMessage: BushMessage): boolean => { + const messageFilter = (filterMessage: BushMessage): boolean => { const shouldFilter = new Array<boolean>(); if (args.bot) { shouldFilter.push(filterMessage.author.bot); } return shouldFilter.filter((bool) => bool === false).length === 0; }; + const messages = (await message.channel.messages.fetch({ limit: args.amount })).filter((message) => messageFilter(message)); - const purged = await message.channel.bulkDelete(messages, true).catch(() => {}); - if (!purged) return message.util.reply(`${util.emojis.error} Failed to purge messages.`).catch(() => {}); + const purged = await message.channel.bulkDelete(messages, true).catch(() => null); + if (!purged) return message.util.reply(`${util.emojis.error} Failed to purge messages.`).catch(() => null); else { + client.emit('bushPurge', message.author, message.guild!, message.channel, messages); await message.util .send(`${util.emojis.success} Successfully purged **${purged.size}** messages.`) .then(async (purgeMessage) => { diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/unban.ts index 3436da6..5025ede 100644 --- a/src/commands/moderation/unban.ts +++ b/src/commands/moderation/unban.ts @@ -59,7 +59,7 @@ export default class UnbanCommand extends BushCommand { user = util.resolveUser(user, client.users.cache) as BushUser; } - const responseCode = await message.guild!.unban({ + const responseCode = await message.guild!.bushUnban({ user, moderator: message.author, reason diff --git a/src/commands/moulberry-bush/report.ts b/src/commands/moulberry-bush/report.ts index e387e7d..a5c4cb2 100644 --- a/src/commands/moulberry-bush/report.ts +++ b/src/commands/moulberry-bush/report.ts @@ -1,6 +1,6 @@ import { GuildMember, MessageEmbed } from 'discord.js'; import moment from 'moment'; -import { AllowedMentions, BushCommand, BushMessage, BushTextChannel } from '../../lib'; +import { AllowedMentions, BushCommand, BushMessage } from '../../lib'; export default class ReportCommand extends BushCommand { public constructor() { @@ -71,9 +71,11 @@ export default class ReportCommand extends BushCommand { if (member.user.bot) return await message.util.reply(`${util.emojis.error} You cannot report a bot <:WeirdChamp:756283321301860382>.`); - const reportChannelId = (await message.guild.getSetting('logChannels')).report; - if (!reportChannelId) - return await message.util.reply(`${util.emojis.error} This server has not setup a report logging channel.`); + const reportChannel = await message.guild.getLogChannel('report'); + if (!reportChannel) + return await message.util.reply( + `${util.emojis.error} This server has not setup a report logging channel or the channel no longer exists.` + ); //The formatting of the report is mostly copied from carl since it is pretty good when it actually works const reportEmbed = new MessageEmbed() @@ -109,7 +111,6 @@ export default class ReportCommand extends BushCommand { reportEmbed.addField('Attachment', message.attachments.first()!.url); } } - const reportChannel = client.channels.cache.get(reportChannelId) as unknown as BushTextChannel; await reportChannel.send({ embeds: [reportEmbed] }).then(async (ReportMessage) => { try { await ReportMessage.react(util.emojis.check); diff --git a/src/lib/extensions/discord-akairo/BushClient.ts b/src/lib/extensions/discord-akairo/BushClient.ts index 5c1cb35..59c4df8 100644 --- a/src/lib/extensions/discord-akairo/BushClient.ts +++ b/src/lib/extensions/discord-akairo/BushClient.ts @@ -1,6 +1,7 @@ import chalk from 'chalk'; import { AkairoClient, ContextMenuCommandHandler } from 'discord-akairo'; import { + Awaited, Collection, Intents, InteractionReplyOptions, @@ -48,6 +49,7 @@ import { BushButtonInteraction } from '../discord.js/BushButtonInteraction'; import { BushCategoryChannel } from '../discord.js/BushCategoryChannel'; import { BushChannel } from '../discord.js/BushChannel'; import { BushChannelManager } from '../discord.js/BushChannelManager'; +import { BushClientEvents } from '../discord.js/BushClientEvents'; import { BushClientUser } from '../discord.js/BushClientUser'; import { BushCommandInteraction } from '../discord.js/BushCommandInteraction'; import { BushDMChannel } from '../discord.js/BushDMChannel'; @@ -159,6 +161,50 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re public logger = BushLogger; public constants = BushConstants; public cache = BushCache; + + public override on<K extends keyof BushClientEvents>( + event: K, + listener: (...args: BushClientEvents[K]) => Awaited<void> + ): this; + public override on<S extends string | symbol>( + event: Exclude<S, keyof BushClientEvents>, + listener: (...args: any[]) => Awaited<void> + ): this { + return super.on(event as any, listener); + } + + public override once<K extends keyof BushClientEvents>( + event: K, + listener: (...args: BushClientEvents[K]) => Awaited<void> + ): this; + public override once<S extends string | symbol>( + event: Exclude<S, keyof BushClientEvents>, + listener: (...args: any[]) => Awaited<void> + ): this { + return super.once(event as any, listener); + } + + public override emit<K extends keyof BushClientEvents>(event: K, ...args: BushClientEvents[K]): boolean; + public override emit<S extends string | symbol>(event: Exclude<S, keyof BushClientEvents>, ...args: unknown[]): boolean { + return super.emit(event as any, ...args); + } + + public override off<K extends keyof BushClientEvents>( + event: K, + listener: (...args: BushClientEvents[K]) => Awaited<void> + ): this; + public override off<S extends string | symbol>( + event: Exclude<S, keyof BushClientEvents>, + listener: (...args: any[]) => Awaited<void> + ): this { + return super.off(event as any, listener); + } + + public override removeAllListeners<K extends keyof BushClientEvents>(event?: K): this; + public override removeAllListeners<S extends string | symbol>(event?: Exclude<S, keyof BushClientEvents>): this { + return super.removeAllListeners(event as any); + } + public constructor(config: Config) { super({ ownerID: config.owners, diff --git a/src/lib/extensions/discord-akairo/BushClientUtil.ts b/src/lib/extensions/discord-akairo/BushClientUtil.ts index 3f9e0b6..fec0174 100644 --- a/src/lib/extensions/discord-akairo/BushClientUtil.ts +++ b/src/lib/extensions/discord-akairo/BushClientUtil.ts @@ -8,6 +8,7 @@ import { BushGuildResolvable, BushMessage, BushSlashMessage, + BushUser, Global, Guild, ModLog, @@ -576,7 +577,7 @@ export class BushClientUtil extends ClientUtil { if (content.length > 400_000 && !substr) { void this.handleError('haste', new Error(`content over 400,000 characters (${content.length.toLocaleString()})`)); return { error: 'content too long' }; - } else { + } else if (content.length > 400_000) { content = content.substr(0, 400_000); isSubstr = true; } @@ -876,7 +877,7 @@ export class BushClientUtil extends ClientUtil { const haste = await this.haste(code, substr); hasteOut = `Too large to display. ${ haste.url - ? `Hastebin: ${haste.url}${haste.error ? `(${haste.error})` : ''}` + ? `Hastebin: ${haste.url}${haste.error ? ` - ${haste.error}` : ''}` : `${this.emojis.error} Hastebin: ${haste.error}` }`; } @@ -969,7 +970,7 @@ export class BushClientUtil extends ClientUtil { public async inspectCleanRedactHaste(input: any, inspectOptions?: BushInspectOptions) { input = typeof input !== 'string' ? this.inspect(input, inspectOptions ?? undefined) : input; input = this.redact(input); - return this.haste(input); + return this.haste(input, true); } public inspectAndRedact(input: any, inspectOptions?: BushInspectOptions) { @@ -1413,7 +1414,7 @@ export class BushClientUtil extends ClientUtil { }); } - public async resolveNonCachedUser(user: UserResolvable | undefined | null): Promise<User | undefined> { + 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 diff --git a/src/lib/extensions/discord-akairo/BushCommand.ts b/src/lib/extensions/discord-akairo/BushCommand.ts index 495a454..1c8ea5b 100644 --- a/src/lib/extensions/discord-akairo/BushCommand.ts +++ b/src/lib/extensions/discord-akairo/BushCommand.ts @@ -147,7 +147,7 @@ export interface BushCommandOptions extends CommandOptions { }; args?: BushArgumentOptions[] & CustomBushArgumentOptions[]; category: string; - completelyHide?: boolean; + pseudo?: boolean; } export class BushCommand extends Command { @@ -166,8 +166,8 @@ export class BushCommand extends Command { /** Whether the command is hidden from the help command. */ public hidden: boolean; - /** Completely hide this command from the help command. */ - public completelyHide: boolean; + /** A fake command, completely hidden from the help command. */ + public pseudo: boolean; public constructor(id: string, options: BushCommandOptions) { if (options.args && typeof options.args !== 'function') { @@ -184,7 +184,7 @@ export class BushCommand extends Command { this.hidden = options.hidden ?? false; this.restrictedChannels = options.restrictedChannels!; this.restrictedGuilds = options.restrictedGuilds!; - this.completelyHide = options.completelyHide!; + this.pseudo = options.pseudo!; } public override exec(message: BushMessage, args: any): any; diff --git a/src/lib/extensions/discord.js/BushClientEvents.d.ts b/src/lib/extensions/discord.js/BushClientEvents.d.ts index ae9b186..8695e7a 100644 --- a/src/lib/extensions/discord.js/BushClientEvents.d.ts +++ b/src/lib/extensions/discord.js/BushClientEvents.d.ts @@ -9,7 +9,10 @@ import { Sticker, Typing } from 'discord.js'; -import { BushClient, BushTextBasedChannels } from '../discord-akairo/BushClient'; +import { + BushClient, + BushTextBasedChannels +} from '../discord-akairo/BushClient'; import { BushApplicationCommand } from './BushApplicationCommand'; import { BushDMChannel } from './BushDMChannel'; import { BushGuild } from './BushGuild'; @@ -18,7 +21,11 @@ import { BushGuildChannel } from './BushGuildChannel'; import { BushGuildEmoji } from './BushGuildEmoji'; import { BushGuildMember, PartialBushGuildMember } from './BushGuildMember'; import { BushMessage, PartialBushMessage } from './BushMessage'; -import { BushMessageReaction, PartialBushMessageReaction } from './BushMessageReaction'; +import { + BushMessageReaction, + PartialBushMessageReaction +} from './BushMessageReaction'; +import { BushNewsChannel } from './BushNewsChannel'; import { BushPresence } from './BushPresence'; import { BushRole } from './BushRole'; import { BushStageInstance } from './BushStageInstance'; @@ -31,11 +38,17 @@ import { BushVoiceState } from './BushVoiceState'; export interface BushClientEvents extends ClientEvents { applicationCommandCreate: [command: BushApplicationCommand]; applicationCommandDelete: [command: BushApplicationCommand]; - applicationCommandUpdate: [oldCommand: BushApplicationCommand | null, newCommand: BushApplicationCommand]; + applicationCommandUpdate: [ + oldCommand: BushApplicationCommand | null, + newCommand: BushApplicationCommand + ]; channelCreate: [channel: BushGuildChannel]; channelDelete: [channel: BushDMChannel | BushGuildChannel]; channelPinsUpdate: [channel: BushTextBasedChannels, date: Date]; - channelUpdate: [oldChannel: BushDMChannel | BushGuildChannel, newChannel: BushDMChannel | BushGuildChannel]; + channelUpdate: [ + oldChannel: BushDMChannel | BushGuildChannel, + newChannel: BushDMChannel | BushGuildChannel + ]; debug: [message: string]; warn: [message: string]; emojiCreate: [emoji: BushGuildEmoji]; @@ -54,20 +67,40 @@ export interface BushClientEvents extends ClientEvents { guildMembersChunk: [ members: Collection<Snowflake, BushGuildMember>, guild: BushGuild, - data: { count: number; index: number; nonce: string | undefined } + data: { + count: number; + index: number; + nonce: string | undefined; + } + ]; + guildMemberUpdate: [ + oldMember: BushGuildMember | PartialBushGuildMember, + newMember: BushGuildMember ]; - guildMemberUpdate: [oldMember: BushGuildMember | PartialBushGuildMember, newMember: BushGuildMember]; guildUpdate: [oldGuild: BushGuild, newGuild: BushGuild]; inviteCreate: [invite: Invite]; inviteDelete: [invite: Invite]; messageCreate: [message: BushMessage]; messageDelete: [message: BushMessage | PartialBushMessage]; messageReactionRemoveAll: [message: BushMessage | PartialBushMessage]; - messageReactionRemoveEmoji: [reaction: BushMessageReaction | PartialBushMessageReaction]; - messageDeleteBulk: [messages: Collection<Snowflake, BushMessage | PartialBushMessage>]; - messageReactionAdd: [reaction: BushMessageReaction | PartialBushMessageReaction, user: BushUser | PartialBushUser]; - messageReactionRemove: [reaction: BushMessageReaction | PartialBushMessageReaction, user: BushUser | PartialBushUser]; - messageUpdate: [oldMessage: BushMessage | PartialBushMessage, newMessage: BushMessage | PartialBushMessage]; + messageReactionRemoveEmoji: [ + reaction: BushMessageReaction | PartialBushMessageReaction + ]; + messageDeleteBulk: [ + messages: Collection<Snowflake, BushMessage | PartialBushMessage> + ]; + messageReactionAdd: [ + reaction: BushMessageReaction | PartialBushMessageReaction, + user: BushUser | PartialBushUser + ]; + messageReactionRemove: [ + reaction: BushMessageReaction | PartialBushMessageReaction, + user: BushUser | PartialBushUser + ]; + messageUpdate: [ + oldMessage: BushMessage | PartialBushMessage, + newMessage: BushMessage | PartialBushMessage + ]; presenceUpdate: [oldPresence: BushPresence | null, newPresence: BushPresence]; rateLimit: [rateLimitData: RateLimitData]; invalidRequestWarning: [invalidRequestWarningData: InvalidRequestWarningData]; @@ -79,7 +112,10 @@ export interface BushClientEvents extends ClientEvents { threadCreate: [thread: BushThreadChannel]; threadDelete: [thread: BushThreadChannel]; threadListSync: [threads: Collection<Snowflake, BushThreadChannel>]; - threadMemberUpdate: [oldMember: BushThreadMember, newMember: BushThreadMember]; + threadMemberUpdate: [ + oldMember: BushThreadMember, + newMember: BushThreadMember + ]; threadMembersUpdate: [ oldMembers: Collection<Snowflake, BushThreadMember>, mewMembers: Collection<Snowflake, BushThreadMember> @@ -95,9 +131,86 @@ export interface BushClientEvents extends ClientEvents { shardReconnecting: [shardId: number]; shardResume: [shardId: number, replayedEvents: number]; stageInstanceCreate: [stageInstance: BushStageInstance]; - stageInstanceUpdate: [oldStageInstance: BushStageInstance | null, newStageInstance: BushStageInstance]; + stageInstanceUpdate: [ + oldStageInstance: BushStageInstance | null, + newStageInstance: BushStageInstance + ]; stageInstanceDelete: [stageInstance: BushStageInstance]; stickerCreate: [sticker: Sticker]; stickerDelete: [sticker: Sticker]; stickerUpdate: [oldSticker: Sticker, newSticker: Sticker]; + /* Custom */ + bushBan: [ + victim: BushGuildMember | BushUser, + moderator: BushUser, + guild: BushGuild, + reason: string | undefined, + caseID: string, + duration: number, + dmSuccess?: boolean + ]; + bushKick: [ + victim: BushGuildMember, + moderator: BushUser, + guild: BushGuild, + reason: string | undefined, + caseID: string, + dmSuccess: boolean + ]; + bushMute: [ + victim: BushGuildMember, + moderator: BushUser, + guild: BushGuild, + reason: string | undefined, + caseID: string, + duration: number, + dmSuccess: boolean + ]; + bushPunishRole: [ + victim: BushGuildMember, + moderator: BushUser, + guild: BushGuild, + reason: string | undefined, + caseID: string, + duration: number, + role: BushRole + ]; + bushPunishRoleRemove: [ + victim: BushGuildMember, + moderator: BushUser, + guild: BushGuild, + caseID: string, + reason: string | undefined, + role: BushRole + ]; + bushPurge: [ + moderator: BushUser, + guild: BushGuild, + channel: BushTextChannel | BushNewsChannel | BushThreadChannel, + messages: Collection<Snowflake, BushMessage> + ]; + bushUnban: [ + victim: BushUser, + moderator: BushUser, + guild: BushGuild, + reason: string | undefined, + caseID: string, + dmSuccess: boolean + ]; + bushUnmute: [ + victim: BushGuildMember, + moderator: BushUser, + guild: BushGuild, + reason: string | undefined, + caseID: string, + dmSuccess: boolean + ]; + bushWarn: [ + victim: BushGuildMember, + moderator: BushUser, + guild: BushGuild, + reason: string | undefined, + caseID: string, + dmSuccess: boolean + ]; } diff --git a/src/lib/extensions/discord.js/BushGuild.ts b/src/lib/extensions/discord.js/BushGuild.ts index efecdcd..18f6542 100644 --- a/src/lib/extensions/discord.js/BushGuild.ts +++ b/src/lib/extensions/discord.js/BushGuild.ts @@ -1,10 +1,11 @@ import { Guild, UserResolvable } from 'discord.js'; import { RawGuildData } from 'discord.js/typings/rawDataTypes'; -import { Guild as GuildDB, GuildFeatures, GuildModel } from '../../models/Guild'; +import { Guild as GuildDB, GuildFeatures, GuildLogType, GuildModel } from '../../models/Guild'; import { ModLogType } from '../../models/ModLog'; import { BushClient, BushUserResolvable } from '../discord-akairo/BushClient'; import { BushGuildMember } from './BushGuildMember'; import { BushGuildMemberManager } from './BushGuildMemberManager'; +import { BushTextChannel } from './BushTextChannel'; import { BushUser } from './BushUser'; export class BushGuild extends Guild { @@ -50,7 +51,17 @@ export class BushGuild extends Guild { return await row.save(); } - public async ban(options: { + public async getLogChannel(logType: GuildLogType): Promise<BushTextChannel | undefined> { + const channelId = (await this.getSetting('logChannels'))[logType]; + if (!channelId) return undefined; + return ( + (this.channels.cache.get(channelId) as BushTextChannel | undefined) ?? + ((await this.channels.fetch(channelId)) as BushTextChannel | null) ?? + undefined + ); + } + + public async bushBan(options: { user: BushUserResolvable | UserResolvable; reason?: string | null; moderator?: BushUserResolvable; @@ -62,42 +73,51 @@ export class BushGuild extends Guild { // checks if (!this.me!.permissions.has('BAN_MEMBERS')) return 'missing permissions'; + let caseID: string | undefined = undefined; + const user = (await util.resolveNonCachedUser(options.user))!; const moderator = (await util.resolveNonCachedUser(options.moderator!)) ?? client.user!; - // ban - const banSuccess = await this.bans - .create(options.user, { - reason: `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`, - days: options.deleteDays - }) - .catch(() => false); - if (!banSuccess) return 'error banning'; - - // add modlog entry - const { log: modlog } = await util.createModLogEntry({ - type: options.duration ? ModLogType.TEMP_BAN : ModLogType.PERM_BAN, - user: options.user as BushUserResolvable, - moderator: moderator.id, - reason: options.reason, - duration: options.duration, - guild: this - }); - if (!modlog) return 'error creating modlog entry'; - - // add punishment entry so they can be unbanned later - const punishmentEntrySuccess = await util.createPunishmentEntry({ - type: 'ban', - user: options.user as BushUserResolvable, - guild: this, - duration: options.duration, - modlog: modlog.id - }); - if (!punishmentEntrySuccess) return 'error creating ban entry'; - - return 'success'; + const ret = await (async () => { + // ban + const banSuccess = await this.bans + .create(user?.id ?? options.user, { + reason: `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`, + days: options.deleteDays + }) + .catch(() => false); + if (!banSuccess) return 'error banning'; + + // add modlog entry + const { log: modlog } = await util.createModLogEntry({ + type: options.duration ? ModLogType.TEMP_BAN : ModLogType.PERM_BAN, + user: user, + moderator: moderator.id, + reason: options.reason, + duration: options.duration, + guild: this + }); + if (!modlog) return 'error creating modlog entry'; + caseID = modlog.id; + + // add punishment entry so they can be unbanned later + const punishmentEntrySuccess = await util.createPunishmentEntry({ + type: 'ban', + user: user, + guild: this, + duration: options.duration, + modlog: modlog.id + }); + if (!punishmentEntrySuccess) return 'error creating ban entry'; + + return 'success'; + })(); + + if (!['error banning', 'error creating modlog entry', 'error creating ban entry'].includes(ret)) + client.emit('bushBan', user, moderator, this, options.reason ?? undefined, caseID!, options.duration ?? 0); + return ret; } - public async unban(options: { + public async bushUnban(options: { user: BushUserResolvable | BushUser; reason?: string | null; moderator?: BushUserResolvable; @@ -109,48 +129,59 @@ export class BushGuild extends Guild { | 'error creating modlog entry' | 'error removing ban entry' > { + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; const user = (await util.resolveNonCachedUser(options.user))!; const moderator = (await util.resolveNonCachedUser(options.moderator ?? this.me))!; - const bans = await this.bans.fetch(); - - let notBanned = false; - if (!bans.has(user.id)) notBanned = true; - - const unbanSuccess = await this.bans - .remove(user, `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`) - .catch((e) => { - if (e?.code === 'UNKNOWN_BAN') { - notBanned = true; - return true; - } else return false; + const ret = await (async () => { + const bans = await this.bans.fetch(); + + let notBanned = false; + if (!bans.has(user.id)) notBanned = true; + + const unbanSuccess = await this.bans + .remove(user, `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`) + .catch((e) => { + if (e?.code === 'UNKNOWN_BAN') { + notBanned = true; + return true; + } else return false; + }); + + if (notBanned) return 'user not banned'; + if (!unbanSuccess) return 'error unbanning'; + + // add modlog entry + const { log: modlog } = await util.createModLogEntry({ + type: ModLogType.UNBAN, + user: user.id, + moderator: moderator.id, + reason: options.reason, + guild: this }); + if (!modlog) return 'error creating modlog entry'; + caseID = modlog.id; + + // remove punishment entry + const removePunishmentEntrySuccess = await util.removePunishmentEntry({ + type: 'ban', + user: user.id, + guild: this + }); + if (!removePunishmentEntrySuccess) return 'error removing ban entry'; - if (!unbanSuccess) return 'error unbanning'; - - // add modlog entry - const modlog = await util.createModLogEntry({ - type: ModLogType.UNBAN, - user: user.id, - moderator: moderator.id, - reason: options.reason, - guild: this - }); - if (!modlog) return 'error creating modlog entry'; - - // remove punishment entry - const removePunishmentEntrySuccess = await util.removePunishmentEntry({ - type: 'ban', - user: user.id, - guild: this - }); - if (!removePunishmentEntrySuccess) return 'error removing ban entry'; - - const userObject = client.users.cache.get(user.id); + const userObject = client.users.cache.get(user.id); - userObject?.send(`You have been unbanned from **${this}** for **${options.reason ?? 'No reason provided'}**.`); + const dmSuccess = await userObject + ?.send(`You have been unbanned from **${this}** for **${options.reason ?? 'No reason provided'}**.`) + .catch(() => false); + dmSuccessEvent = !!dmSuccess; - if (notBanned) return 'user not banned'; - return 'success'; + return 'success'; + })(); + if (!['error unbanning', 'error creating modlog entry', 'error removing ban entry'].includes(ret)) + client.emit('bushUnban', user, moderator, this, options.reason ?? undefined, caseID!, dmSuccessEvent!); + return ret; } } diff --git a/src/lib/extensions/discord.js/BushGuildMember.ts b/src/lib/extensions/discord.js/BushGuildMember.ts index ab4eee4..4dd1a5d 100644 --- a/src/lib/extensions/discord.js/BushGuildMember.ts +++ b/src/lib/extensions/discord.js/BushGuildMember.ts @@ -105,96 +105,140 @@ export class BushGuildMember extends GuildMember { } public async warn(options: BushPunishmentOptions): Promise<{ result: WarnResponse | null; caseNum: number | null }> { + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; const moderator = (await util.resolveNonCachedUser(options.moderator ?? this.guild.me))!; - // add modlog entry - const result = await util.createModLogEntry( - { - type: ModLogType.WARN, - user: this, - moderator: moderator.id, - reason: options.reason, - guild: this.guild - }, - true - ); - if (!result || !result.log) return { result: 'error creating modlog entry', caseNum: null }; - - // dm user - const dmSuccess = await this.punishDM('warned', options.reason); - if (!dmSuccess) return { result: 'failed to dm', caseNum: result.caseNum }; - - return { result: 'success', caseNum: result.caseNum }; + const ret = await (async () => { + // add modlog entry + const result = await util.createModLogEntry( + { + type: ModLogType.WARN, + user: this, + moderator: moderator.id, + reason: options.reason, + guild: this.guild + }, + true + ); + caseID = result.log?.id; + if (!result || !result.log) return { result: 'error creating modlog entry', caseNum: null }; + + // dm user + const dmSuccess = await this.punishDM('warned', options.reason); + dmSuccessEvent = dmSuccess; + if (!dmSuccess) return { result: 'failed to dm', caseNum: result.caseNum }; + + return { result: 'success', caseNum: result.caseNum }; + })(); + if (!['error creating modlog entry'].includes(ret.result)) + client.emit('bushWarn', this, moderator, this.guild, options.reason ?? undefined, caseID!, dmSuccessEvent!); + return ret as { result: WarnResponse | null; caseNum: number | null }; } public async addRole(options: AddRoleOptions): Promise<AddRoleResponse> { const ifShouldAddRole = this.#checkIfShouldAddRole(options.role); if (ifShouldAddRole !== true) return ifShouldAddRole; + let caseID: string | undefined = undefined; const moderator = (await util.resolveNonCachedUser(options.moderator ?? this.guild.me))!; - if (options.addToModlog || options.duration) { - const { log: modlog } = options.addToModlog - ? await util.createModLogEntry({ - type: options.duration ? ModLogType.TEMP_PUNISHMENT_ROLE : ModLogType.PERM_PUNISHMENT_ROLE, - guild: this.guild, - moderator: moderator.id, - user: this, - reason: 'N/A' - }) - : { log: null }; - - if (!modlog && options.addToModlog) return 'error creating modlog entry'; - + const ret = await (async () => { if (options.addToModlog || options.duration) { - const punishmentEntrySuccess = await util.createPunishmentEntry({ - type: 'role', - user: this, - guild: this.guild, - modlog: modlog?.id ?? undefined, - duration: options.duration, - extraInfo: options.role.id - }); - if (!punishmentEntrySuccess) return 'error creating role entry'; + const { log: modlog } = options.addToModlog + ? await util.createModLogEntry({ + type: options.duration ? ModLogType.TEMP_PUNISHMENT_ROLE : ModLogType.PERM_PUNISHMENT_ROLE, + guild: this.guild, + moderator: moderator.id, + user: this, + reason: 'N/A' + }) + : { log: null }; + caseID = modlog?.id; + + if (!modlog && options.addToModlog) return 'error creating modlog entry'; + + if (options.addToModlog || options.duration) { + const punishmentEntrySuccess = await util.createPunishmentEntry({ + type: 'role', + user: this, + guild: this.guild, + modlog: modlog?.id ?? undefined, + duration: options.duration, + extraInfo: options.role.id + }); + if (!punishmentEntrySuccess) return 'error creating role entry'; + } } - } - - const removeRoleSuccess = await this.roles.add(options.role, `${moderator.tag}`); - if (!removeRoleSuccess) return 'error adding role'; - return 'success'; + const removeRoleSuccess = await this.roles.add(options.role, `${moderator.tag}`); + if (!removeRoleSuccess) return 'error adding role'; + + return 'success'; + })(); + if (!['error adding role', 'error creating modlog entry', 'error creating role entry'].includes(ret) && options.addToModlog) + client.emit( + 'bushPunishRole', + this, + moderator, + this.guild, + options.reason ?? undefined, + caseID!, + options.duration ?? 0, + options.role as BushRole + ); + return ret; } public async removeRole(options: RemoveRoleOptions): Promise<RemoveRoleResponse> { const ifShouldAddRole = this.#checkIfShouldAddRole(options.role); if (ifShouldAddRole !== true) return ifShouldAddRole; + let caseID: string | undefined = undefined; const moderator = (await util.resolveNonCachedUser(options.moderator ?? this.guild.me))!; - if (options.addToModlog) { - const { log: modlog } = await util.createModLogEntry({ - type: ModLogType.PERM_PUNISHMENT_ROLE, - guild: this.guild, - moderator: moderator.id, - user: this, - reason: 'N/A' - }); - - if (!modlog) return 'error creating modlog entry'; + const ret = await (async () => { + if (options.addToModlog) { + const { log: modlog } = await util.createModLogEntry({ + type: ModLogType.PERM_PUNISHMENT_ROLE, + guild: this.guild, + moderator: moderator.id, + user: this, + reason: 'N/A' + }); - const punishmentEntrySuccess = await util.removePunishmentEntry({ - type: 'role', - user: this, - guild: this.guild - }); + if (!modlog) return 'error creating modlog entry'; + caseID = modlog.id; - if (!punishmentEntrySuccess) return 'error removing role entry'; - } + const punishmentEntrySuccess = await util.removePunishmentEntry({ + type: 'role', + user: this, + guild: this.guild + }); - const removeRoleSuccess = await this.roles.remove(options.role, `${moderator.tag}`); - if (!removeRoleSuccess) return 'error removing role'; + if (!punishmentEntrySuccess) return 'error removing role entry'; + } - return 'success'; + const removeRoleSuccess = await this.roles.remove(options.role, `${moderator.tag}`); + if (!removeRoleSuccess) return 'error removing role'; + + return 'success'; + })(); + + if ( + !['error removing role', 'error creating modlog entry', 'error removing role entry'].includes(ret) && + options.addToModlog + ) + client.emit( + 'bushPunishRoleRemove', + this, + moderator, + this.guild, + caseID!, + options.reason ?? undefined, + options.role as BushRole + ); + return ret; } #checkIfShouldAddRole(role: BushRole | Role): true | 'user hierarchy' | 'role managed' | 'client hierarchy' { @@ -217,47 +261,66 @@ export class BushGuildMember extends GuildMember { if (!muteRole) return 'invalid mute role'; if (muteRole.position >= this.guild.me!.roles.highest.position || muteRole.managed) return 'mute role not manageable'; + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; const moderator = (await util.resolveNonCachedUser(options.moderator ?? this.guild.me))!; - // add role - const muteSuccess = await this.roles - .add(muteRole, `[Mute] ${moderator.tag} | ${options.reason ?? 'No reason provided.'}`) - .catch(async (e) => { - await client.console.warn('muteRoleAddError', e); - client.console.debug(e); - return false; - }); - if (!muteSuccess) return 'error giving mute role'; - - // add modlog entry - const { log: modlog } = await util.createModLogEntry({ - type: options.duration ? ModLogType.TEMP_MUTE : ModLogType.PERM_MUTE, - user: this, - moderator: moderator.id, - reason: options.reason, - duration: options.duration, - guild: this.guild - }); - - if (!modlog) return 'error creating modlog entry'; - - // add punishment entry so they can be unmuted later - const punishmentEntrySuccess = await util.createPunishmentEntry({ - type: 'mute', - user: this, - guild: this.guild, - duration: options.duration, - modlog: modlog.id - }); + const ret = await (async () => { + // add role + const muteSuccess = await this.roles + .add(muteRole, `[Mute] ${moderator.tag} | ${options.reason ?? 'No reason provided.'}`) + .catch(async (e) => { + await client.console.warn('muteRoleAddError', e); + client.console.debug(e); + return false; + }); + if (!muteSuccess) return 'error giving mute role'; - if (!punishmentEntrySuccess) return 'error creating mute entry'; + // add modlog entry + const { log: modlog } = await util.createModLogEntry({ + type: options.duration ? ModLogType.TEMP_MUTE : ModLogType.PERM_MUTE, + user: this, + moderator: moderator.id, + reason: options.reason, + duration: options.duration, + guild: this.guild + }); - // dm user - const dmSuccess = await this.punishDM('muted', options.reason, options.duration ?? 0); + if (!modlog) return 'error creating modlog entry'; + caseID = modlog.id; - if (!dmSuccess) return 'failed to dm'; + // add punishment entry so they can be unmuted later + const punishmentEntrySuccess = await util.createPunishmentEntry({ + type: 'mute', + user: this, + guild: this.guild, + duration: options.duration, + modlog: modlog.id + }); - return 'success'; + if (!punishmentEntrySuccess) return 'error creating mute entry'; + + // dm user + const dmSuccess = await this.punishDM('muted', options.reason, options.duration ?? 0); + dmSuccessEvent = dmSuccess; + + if (!dmSuccess) return 'failed to dm'; + + return 'success'; + })(); + + if (!['error giving mute role', 'error creating modlog entry', 'error creating mute entry'].includes(ret)) + client.emit( + 'bushMute', + this, + moderator, + this.guild, + options.reason ?? undefined, + caseID!, + options.duration ?? 0, + dmSuccessEvent! + ); + return ret; } public async unmute(options: BushPunishmentOptions): Promise<UnmuteResponse> { @@ -269,110 +332,145 @@ export class BushGuildMember extends GuildMember { if (!muteRole) return 'invalid mute role'; if (muteRole.position >= this.guild.me!.roles.highest.position || muteRole.managed) return 'mute role not manageable'; + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; const moderator = (await util.resolveNonCachedUser(options.moderator ?? this.guild.me))!; - //remove role - const muteSuccess = await this.roles - .remove(muteRole, `[Unmute] ${moderator.tag} | ${options.reason ?? 'No reason provided.'}`) - .catch(async (e) => { - await client.console.warn('muteRoleAddError', e?.stack || e); - return false; + const ret = await (async () => { + //remove role + const muteSuccess = await this.roles + .remove(muteRole, `[Unmute] ${moderator.tag} | ${options.reason ?? 'No reason provided.'}`) + .catch(async (e) => { + await client.console.warn('muteRoleAddError', e?.stack || e); + return false; + }); + if (!muteSuccess) return 'error removing mute role'; + + //remove modlog entry + const { log: modlog } = await util.createModLogEntry({ + type: ModLogType.UNMUTE, + user: this, + moderator: moderator.id, + reason: options.reason, + guild: this.guild }); - if (!muteSuccess) return 'error removing mute role'; - //remove modlog entry - const { log: modlog } = await util.createModLogEntry({ - type: ModLogType.UNMUTE, - user: this, - moderator: moderator.id, - reason: options.reason, - guild: this.guild - }); + if (!modlog) return 'error creating modlog entry'; + caseID = modlog.id; - if (!modlog) return 'error creating modlog entry'; + // remove mute entry + const removePunishmentEntrySuccess = await util.removePunishmentEntry({ + type: 'mute', + user: this, + guild: this.guild + }); - // remove mute entry - const removePunishmentEntrySuccess = await util.removePunishmentEntry({ - type: 'mute', - user: this, - guild: this.guild - }); + if (!removePunishmentEntrySuccess) return 'error removing mute entry'; - if (!removePunishmentEntrySuccess) return 'error removing mute entry'; + //dm user + const dmSuccess = await this.punishDM('unmuted', options.reason, undefined, false); + dmSuccessEvent = dmSuccess; - //dm user - const dmSuccess = await this.punishDM('unmuted', options.reason, undefined, false); + if (!dmSuccess) return 'failed to dm'; - if (!dmSuccess) return 'failed to dm'; + return 'success'; + })(); - return 'success'; + if (!['error removing mute role', 'error creating modlog entry', 'error removing mute entry'].includes(ret)) + client.emit('bushUnmute', this, moderator, this.guild, options.reason ?? undefined, caseID!, dmSuccessEvent!); + return ret; } public async bushKick(options: BushPunishmentOptions): Promise<KickResponse> { // checks if (!this.guild.me?.permissions.has('KICK_MEMBERS') || !this.kickable) return 'missing permissions'; + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; const moderator = (await util.resolveNonCachedUser(options.moderator ?? this.guild.me))!; + const ret = await (async () => { + // dm user + const dmSuccess = await this.punishDM('kicked', options.reason); + dmSuccessEvent = dmSuccess; - // dm user - const dmSuccess = await this.punishDM('kicked', options.reason); - - // kick - const kickSuccess = await this.kick(`${moderator?.tag} | ${options.reason ?? 'No reason provided.'}`).catch(() => false); - if (!kickSuccess) return 'error kicking'; - - // add modlog entry - const { log: modlog } = await util.createModLogEntry({ - type: ModLogType.KICK, - user: this, - moderator: moderator.id, - reason: options.reason, - guild: this.guild - }); - if (!modlog) return 'error creating modlog entry'; - if (!dmSuccess) return 'failed to dm'; - return 'success'; + // kick + const kickSuccess = await this.kick(`${moderator?.tag} | ${options.reason ?? 'No reason provided.'}`).catch(() => false); + if (!kickSuccess) return 'error kicking'; + + // add modlog entry + const { log: modlog } = await util.createModLogEntry({ + type: ModLogType.KICK, + user: this, + moderator: moderator.id, + reason: options.reason, + guild: this.guild + }); + if (!modlog) return 'error creating modlog entry'; + caseID = modlog.id; + if (!dmSuccess) return 'failed to dm'; + return 'success'; + })(); + if (!['error kicking', 'error creating modlog entry'].includes(ret)) + client.emit('bushKick', this, moderator, this.guild, options.reason ?? undefined, caseID!, dmSuccessEvent!); + return ret; } public async bushBan(options: BushBanOptions): Promise<BanResponse> { // checks if (!this.guild.me!.permissions.has('BAN_MEMBERS') || !this.bannable) return 'missing permissions'; + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; const moderator = (await util.resolveNonCachedUser(options.moderator ?? this.guild.me))!; + const ret = await (async () => { + // dm user + const dmSuccess = await this.punishDM('banned', options.reason, options.duration ?? 0); + dmSuccessEvent = dmSuccess; + + // ban + const banSuccess = await this.ban({ + reason: `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`, + days: options.deleteDays + }).catch(() => false); + if (!banSuccess) return 'error banning'; + + // add modlog entry + const { log: modlog } = await util.createModLogEntry({ + type: options.duration ? ModLogType.TEMP_BAN : ModLogType.PERM_BAN, + user: this, + moderator: moderator.id, + reason: options.reason, + duration: options.duration, + guild: this.guild + }); + if (!modlog) return 'error creating modlog entry'; + caseID = modlog.id; - // dm user - const dmSuccess = await this.punishDM('banned', options.reason, options.duration ?? 0); - - // ban - const banSuccess = await this.ban({ - reason: `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`, - days: options.deleteDays - }).catch(() => false); - if (!banSuccess) return 'error banning'; - - // add modlog entry - const { log: modlog } = await util.createModLogEntry({ - type: options.duration ? ModLogType.TEMP_BAN : ModLogType.PERM_BAN, - user: this, - moderator: moderator.id, - reason: options.reason, - duration: options.duration, - guild: this.guild - }); - if (!modlog) return 'error creating modlog entry'; - - // add punishment entry so they can be unbanned later - const punishmentEntrySuccess = await util.createPunishmentEntry({ - type: 'ban', - user: this, - guild: this.guild, - duration: options.duration, - modlog: modlog.id - }); - if (!punishmentEntrySuccess) return 'error creating ban entry'; - - if (!dmSuccess) return 'failed to dm'; - return 'success'; + // add punishment entry so they can be unbanned later + const punishmentEntrySuccess = await util.createPunishmentEntry({ + type: 'ban', + user: this, + guild: this.guild, + duration: options.duration, + modlog: modlog.id + }); + if (!punishmentEntrySuccess) return 'error creating ban entry'; + + if (!dmSuccess) return 'failed to dm'; + return 'success'; + })(); + if (!['error banning', 'error creating modlog entry', 'error creating ban entry'].includes(ret)) + client.emit( + 'bushBan', + this, + moderator, + this.guild, + options.reason ?? undefined, + caseID!, + options.duration ?? 0, + dmSuccessEvent! + ); + return ret; } public get isOwner(): boolean { diff --git a/src/lib/models/Guild.ts b/src/lib/models/Guild.ts index 6933794..a4780fd 100644 --- a/src/lib/models/Guild.ts +++ b/src/lib/models/Guild.ts @@ -99,7 +99,7 @@ export const guildLogsObj = { }, moderation: { description: 'Sends a message in this channel every time a moderation action is performed.', - configurable: false + configurable: true }, report: { description: 'Logs user reports.', diff --git a/src/lib/models/ModLog.ts b/src/lib/models/ModLog.ts index 0be1ea7..5c87331 100644 --- a/src/lib/models/ModLog.ts +++ b/src/lib/models/ModLog.ts @@ -2,7 +2,7 @@ import { Snowflake } from 'discord.js'; import { DataTypes, Sequelize } from 'sequelize'; import { v4 as uuidv4 } from 'uuid'; import { BaseModel } from './BaseModel'; -import { NEVER_USED } from './__helpers'; +import { jsonParseGet, jsonParseSet, NEVER_USED } from './__helpers'; export enum ModLogType { PERM_BAN = 'PERM_BAN', @@ -30,6 +30,8 @@ export interface ModLogModel { duration: number | null; guild: Snowflake; evidence: string; + pseudo: boolean; + hidden: boolean; } export interface ModLogModelCreationAttributes { @@ -41,6 +43,8 @@ export interface ModLogModelCreationAttributes { duration?: number; guild: Snowflake; evidence?: string; + pseudo?: boolean; + hidden?: boolean; } export class ModLog extends BaseModel<ModLogModel, ModLogModelCreationAttributes> implements ModLogModel { @@ -124,6 +128,26 @@ export class ModLog extends BaseModel<ModLogModel, ModLogModelCreationAttributes throw new Error(NEVER_USED); } + /** + * Not an actual modlog just used so a punishment entry can be made + */ + public get pseudo(): boolean { + throw new Error(NEVER_USED); + } + public set pseudo(_: boolean) { + throw new Error(NEVER_USED); + } + + /** + * Hides from the modlog command unless show hidden is specified. + */ + public get hidden(): boolean { + throw new Error(NEVER_USED); + } + public set hidden(_: boolean) { + throw new Error(NEVER_USED); + } + public static initModel(sequelize: Sequelize): void { ModLog.init( { @@ -163,6 +187,28 @@ export class ModLog extends BaseModel<ModLogModel, ModLogModelCreationAttributes evidence: { type: DataTypes.TEXT, allowNull: true + }, + pseudo: { + type: DataTypes.STRING, + get: function (): boolean { + return jsonParseGet('pseudo', this); + }, + set: function (val: boolean) { + return jsonParseSet('pseudo', this, val); + }, + allowNull: false, + defaultValue: 'false' + }, + hidden: { + type: DataTypes.STRING, + get: function (): boolean { + return jsonParseGet('hidden', this); + }, + set: function (val: boolean) { + return jsonParseSet('hidden', this, val); + }, + allowNull: false, + defaultValue: 'false' } }, { sequelize: sequelize } diff --git a/src/lib/utils/BushConstants.ts b/src/lib/utils/BushConstants.ts index a8f75fa..f085d26 100644 --- a/src/lib/utils/BushConstants.ts +++ b/src/lib/utils/BushConstants.ts @@ -1,3 +1,5 @@ +import { Constants, ConstantsColors } from 'discord.js'; + interface bushColors { default: '#1FD8F1'; error: '#EF4947'; @@ -21,6 +23,7 @@ interface bushColors { darkGray: '#7a7a7a'; black: '#000000'; orange: '#E86100'; + discord: ConstantsColors; } export type PronounCode = @@ -110,7 +113,8 @@ export class BushConstants { lightGray: '#cfcfcf', darkGray: '#7a7a7a', black: '#000000', - orange: '#E86100' + orange: '#E86100', + discord: Constants.Colors }; // Somewhat stolen from @Mzato0001 diff --git a/src/listeners/custom/bushBan.ts b/src/listeners/custom/bushBan.ts new file mode 100644 index 0000000..a7f30a2 --- /dev/null +++ b/src/listeners/custom/bushBan.ts @@ -0,0 +1,34 @@ +import { BushListener } from '@lib'; +import { GuildMember, MessageEmbed } from 'discord.js'; +import { BushClientEvents } from '../../lib/extensions/discord.js/BushClientEvents'; + +export default class BushBanListener extends BushListener { + public constructor() { + super('bushBan', { + emitter: 'client', + event: 'bushBan', + category: 'custom' + }); + } + + public override async exec( + ...[victim, moderator, guild, reason, caseID, duration, dmSuccess]: BushClientEvents['bushBan'] + ): Promise<unknown> { + const logChannel = await guild.getLogChannel('moderation'); + if (!logChannel) return; + const user = victim instanceof GuildMember ? victim.user : victim; + + const logEmbed = new MessageEmbed() + .setColor(util.colors.discord.RED) + .setTimestamp() + .setFooter(`CaseID: ${caseID}`) + .setAuthor(user.tag, user.avatarURL({ dynamic: true, format: 'png', size: 4096 }) ?? undefined) + .addField('**Action**', `${duration ? 'Temp Ban' : 'Perm Ban'}`, true) + .addField('**User**', `${user} (${user.tag})`, true) + .addField('**Moderator**', `${moderator} (${moderator.tag})`, true) + .addField('**Reason**', `${reason ?? '[No Reason Provided]'}`, true); + if (duration) logEmbed.addField('**Duration**', util.humanizeDuration(duration), true); + if (dmSuccess === false) logEmbed.addField('**Additional Info**', 'Could not dm user.'); + return await logChannel.send({ embeds: [logEmbed] }); + } +} diff --git a/src/listeners/custom/bushKick.ts b/src/listeners/custom/bushKick.ts new file mode 100644 index 0000000..3e586f1 --- /dev/null +++ b/src/listeners/custom/bushKick.ts @@ -0,0 +1,33 @@ +import { BushListener } from '@lib'; +import { GuildMember, MessageEmbed } from 'discord.js'; +import { BushClientEvents } from '../../lib/extensions/discord.js/BushClientEvents'; + +export default class BushKickListener extends BushListener { + public constructor() { + super('bushKick', { + emitter: 'client', + event: 'bushKick', + category: 'custom' + }); + } + + public override async exec( + ...[victim, moderator, guild, reason, caseID, dmSuccess]: BushClientEvents['bushKick'] + ): Promise<unknown> { + const logChannel = await guild.getLogChannel('moderation'); + if (!logChannel) return; + const user = victim instanceof GuildMember ? victim.user : victim; + + const logEmbed = new MessageEmbed() + .setColor(util.colors.discord.RED) + .setTimestamp() + .setFooter(`CaseID: ${caseID}`) + .setAuthor(user.tag, user.avatarURL({ dynamic: true, format: 'png', size: 4096 }) ?? undefined) + .addField('**Action**', `${'Kick'}`, true) + .addField('**User**', `${user} (${user.tag})`, true) + .addField('**Moderator**', `${moderator} (${moderator.tag})`, true) + .addField('**Reason**', `${reason ?? '[No Reason Provided]'}`, true); + if (dmSuccess === false) logEmbed.addField('**Additional Info**', 'Could not dm user.'); + return await logChannel.send({ embeds: [logEmbed] }); + } +} diff --git a/src/listeners/custom/bushMute.ts b/src/listeners/custom/bushMute.ts new file mode 100644 index 0000000..9513c20 --- /dev/null +++ b/src/listeners/custom/bushMute.ts @@ -0,0 +1,34 @@ +import { BushListener } from '@lib'; +import { GuildMember, MessageEmbed } from 'discord.js'; +import { BushClientEvents } from '../../lib/extensions/discord.js/BushClientEvents'; + +export default class BushMuteListener extends BushListener { + public constructor() { + super('bushMute', { + emitter: 'client', + event: 'bushMute', + category: 'custom' + }); + } + + public override async exec( + ...[victim, moderator, guild, reason, caseID, duration, dmSuccess]: BushClientEvents['bushMute'] + ): Promise<unknown> { + const logChannel = await guild.getLogChannel('moderation'); + if (!logChannel) return; + const user = victim instanceof GuildMember ? victim.user : victim; + + const logEmbed = new MessageEmbed() + .setColor(util.colors.discord.ORANGE) + .setTimestamp() + .setFooter(`CaseID: ${caseID}`) + .setAuthor(user.tag, user.avatarURL({ dynamic: true, format: 'png', size: 4096 }) ?? undefined) + .addField('**Action**', `${duration ? 'Temp Mute' : 'Perm Mute'}`, true) + .addField('**User**', `${user} (${user.tag})`, true) + .addField('**Moderator**', `${moderator} (${moderator.tag})`, true) + .addField('**Reason**', `${reason ?? '[No Reason Provided]'}`, true); + if (duration) logEmbed.addField('**Duration**', util.humanizeDuration(duration), true); + if (dmSuccess === false) logEmbed.addField('**Additional Info**', 'Could not dm user.'); + return await logChannel.send({ embeds: [logEmbed] }); + } +} diff --git a/src/listeners/custom/bushPunishRole.ts b/src/listeners/custom/bushPunishRole.ts new file mode 100644 index 0000000..3e7e98f --- /dev/null +++ b/src/listeners/custom/bushPunishRole.ts @@ -0,0 +1,33 @@ +import { BushListener } from '@lib'; +import { GuildMember, MessageEmbed } from 'discord.js'; +import { BushClientEvents } from '../../lib/extensions/discord.js/BushClientEvents'; + +export default class BushPunishRoleListener extends BushListener { + public constructor() { + super('bushPunishRole', { + emitter: 'client', + event: 'bushPunishRole', + category: 'custom' + }); + } + + public override async exec( + ...[victim, moderator, guild, reason, caseID, duration]: BushClientEvents['bushPunishRole'] + ): Promise<unknown> { + const logChannel = await guild.getLogChannel('moderation'); + if (!logChannel) return; + const user = victim instanceof GuildMember ? victim.user : victim; + + const logEmbed = new MessageEmbed() + .setColor(util.colors.discord.YELLOW) + .setTimestamp() + .setFooter(`CaseID: ${caseID}`) + .setAuthor(user.tag, user.avatarURL({ dynamic: true, format: 'png', size: 4096 }) ?? undefined) + .addField('**Action**', `${duration ? 'Temp Punishment Role' : 'Perm Punishment Role'}`, true) + .addField('**User**', `${user} (${user.tag})`, true) + .addField('**Moderator**', `${moderator} (${moderator.tag})`, true) + .addField('**Reason**', `${reason ?? '[No Reason Provided]'}`, true); + if (duration) logEmbed.addField('**Duration**', util.humanizeDuration(duration), true); + return await logChannel.send({ embeds: [logEmbed] }); + } +} diff --git a/src/listeners/custom/bushPunishRoleRemove.ts b/src/listeners/custom/bushPunishRoleRemove.ts new file mode 100644 index 0000000..04d7244 --- /dev/null +++ b/src/listeners/custom/bushPunishRoleRemove.ts @@ -0,0 +1,34 @@ +import { BushListener } from '@lib'; +import { GuildMember, MessageEmbed } from 'discord.js'; +import { BushClientEvents } from '../../lib/extensions/discord.js/BushClientEvents'; + +export default class BushPunishRoleRemoveListener extends BushListener { + public constructor() { + super('bushPunishRoleRemove', { + emitter: 'client', + event: 'bushPunishRoleRemove', + category: 'custom' + }); + } + + public override async exec( + ...[victim, moderator, guild, reason, caseID, role]: BushClientEvents['bushPunishRoleRemove'] + ): Promise<unknown> { + const logChannel = await guild.getLogChannel('moderation'); + if (!logChannel) return; + const user = victim instanceof GuildMember ? victim.user : victim; + + const logEmbed = new MessageEmbed() + .setColor(util.colors.discord.GREEN) + .setTimestamp() + .setFooter(`CaseID: ${caseID}`) + .setAuthor(user.tag, user.avatarURL({ dynamic: true, format: 'png', size: 4096 }) ?? undefined) + .addField('**Action**', `${'Remove Punishment Role'}`, true) + .addField('**Role**', `${role}`, true) + .addField('**User**', `${user} (${user.tag})`, true) + .addField('**Moderator**', `${moderator} (${moderator.tag})`, true) + .addField('**Reason**', `${reason ?? '[No Reason Provided]'}`, true); + + return await logChannel.send({ embeds: [logEmbed] }); + } +} diff --git a/src/listeners/custom/bushPurge.ts b/src/listeners/custom/bushPurge.ts new file mode 100644 index 0000000..cc55fc4 --- /dev/null +++ b/src/listeners/custom/bushPurge.ts @@ -0,0 +1,43 @@ +import { BushListener } from '@lib'; +import { MessageEmbed } from 'discord.js'; +import { BushClientEvents } from '../../lib/extensions/discord.js/BushClientEvents'; + +export default class BushPurgeListener extends BushListener { + public constructor() { + super('bushPurge', { + emitter: 'client', + event: 'bushPurge', + category: 'custom' + }); + } + + public override async exec(...[moderator, guild, channel, messages]: BushClientEvents['bushPurge']): Promise<unknown> { + const logChannel = await guild.getLogChannel('moderation'); + if (!logChannel) return; + + const mappedMessages = messages.map((m) => ({ + id: m.id, + author: `${m.author.tag} (${m.id})`, + content: m.content, + embeds: m.embeds, + attachments: m.attachments + })); + const haste = await util.inspectCleanRedactHaste(mappedMessages); + + const logEmbed = new MessageEmbed() + .setColor(util.colors.discord.DARK_PURPLE) + .setTimestamp() + .setFooter(`${messages.size.toLocaleString()} Messages`) + .addField('**Action**', `${'Purge'}`, true) + .addField('**Moderator**', `${moderator} (${moderator.tag})`, true) + .addField('**Channel**', `<#${channel.id}> (${channel.name})`, true) + .addField( + '**Messages**', + `${ + haste.url ? `[haste](${haste.url})${haste.error ? `- ${haste.error}` : ''}` : `${util.emojis.error} ${haste.error}` + }`, + true + ); + return await logChannel.send({ embeds: [logEmbed] }); + } +} diff --git a/src/listeners/custom/bushUnban.ts b/src/listeners/custom/bushUnban.ts new file mode 100644 index 0000000..ad82979 --- /dev/null +++ b/src/listeners/custom/bushUnban.ts @@ -0,0 +1,33 @@ +import { BushListener } from '@lib'; +import { GuildMember, MessageEmbed } from 'discord.js'; +import { BushClientEvents } from '../../lib/extensions/discord.js/BushClientEvents'; + +export default class BushUnbanListener extends BushListener { + public constructor() { + super('bushUnban', { + emitter: 'client', + event: 'bushUnban', + category: 'custom' + }); + } + + public override async exec( + ...[victim, moderator, guild, reason, caseID, dmSuccess]: BushClientEvents['bushUnban'] + ): Promise<unknown> { + const logChannel = await guild.getLogChannel('moderation'); + if (!logChannel) return; + const user = victim instanceof GuildMember ? victim.user : victim; + + const logEmbed = new MessageEmbed() + .setColor(util.colors.discord.GREEN) + .setTimestamp() + .setFooter(`CaseID: ${caseID}`) + .setAuthor(user.tag, user.avatarURL({ dynamic: true, format: 'png', size: 4096 }) ?? undefined) + .addField('**Action**', `${'Unban'}`, true) + .addField('**User**', `${user} (${user.tag})`, true) + .addField('**Moderator**', `${moderator} (${moderator.tag})`, true) + .addField('**Reason**', `${reason ?? '[No Reason Provided]'}`, true); + if (dmSuccess === false) logEmbed.addField('**Additional Info**', 'Could not dm user.'); + return await logChannel.send({ embeds: [logEmbed] }); + } +} diff --git a/src/listeners/custom/bushUnmute.ts b/src/listeners/custom/bushUnmute.ts new file mode 100644 index 0000000..8beb27b --- /dev/null +++ b/src/listeners/custom/bushUnmute.ts @@ -0,0 +1,33 @@ +import { BushListener } from '@lib'; +import { GuildMember, MessageEmbed } from 'discord.js'; +import { BushClientEvents } from '../../lib/extensions/discord.js/BushClientEvents'; + +export default class BushUnmuteListener extends BushListener { + public constructor() { + super('bushUnmute', { + emitter: 'client', + event: 'bushUnmute', + category: 'custom' + }); + } + + public override async exec( + ...[victim, moderator, guild, reason, caseID, dmSuccess]: BushClientEvents['bushUnmute'] + ): Promise<unknown> { + const logChannel = await guild.getLogChannel('moderation'); + if (!logChannel) return; + const user = victim instanceof GuildMember ? victim.user : victim; + + const logEmbed = new MessageEmbed() + .setColor(util.colors.discord.GREEN) + .setTimestamp() + .setFooter(`CaseID: ${caseID}`) + .setAuthor(user.tag, user.avatarURL({ dynamic: true, format: 'png', size: 4096 }) ?? undefined) + .addField('**Action**', `${'Unmute'}`, true) + .addField('**User**', `${user} (${user.tag})`, true) + .addField('**Moderator**', `${moderator} (${moderator.tag})`, true) + .addField('**Reason**', `${reason ?? '[No Reason Provided]'}`, true); + if (dmSuccess === false) logEmbed.addField('**Additional Info**', 'Could not dm user.'); + return await logChannel.send({ embeds: [logEmbed] }); + } +} diff --git a/src/listeners/custom/bushWarn.ts b/src/listeners/custom/bushWarn.ts new file mode 100644 index 0000000..40c477c --- /dev/null +++ b/src/listeners/custom/bushWarn.ts @@ -0,0 +1,33 @@ +import { BushListener } from '@lib'; +import { GuildMember, MessageEmbed } from 'discord.js'; +import { BushClientEvents } from '../../lib/extensions/discord.js/BushClientEvents'; + +export default class BushWarnListener extends BushListener { + public constructor() { + super('bushWarn', { + emitter: 'client', + event: 'bushWarn', + category: 'custom' + }); + } + + public override async exec( + ...[victim, moderator, guild, reason, caseID, dmSuccess]: BushClientEvents['bushWarn'] + ): Promise<unknown> { + const logChannel = await guild.getLogChannel('moderation'); + if (!logChannel) return; + const user = victim instanceof GuildMember ? victim.user : victim; + + const logEmbed = new MessageEmbed() + .setColor(util.colors.discord.YELLOW) + .setTimestamp() + .setFooter(`CaseID: ${caseID}`) + .setAuthor(user.tag, user.avatarURL({ dynamic: true, format: 'png', size: 4096 }) ?? undefined) + .addField('**Action**', `${'Warn'}`, true) + .addField('**User**', `${user} (${user.tag})`, true) + .addField('**Moderator**', `${moderator} (${moderator.tag})`, true) + .addField('**Reason**', `${reason ?? '[No Reason Provided]'}`, true); + if (dmSuccess === false) logEmbed.addField('**Additional Info**', 'Could not dm user.'); + return await logChannel.send({ embeds: [logEmbed] }); + } +} diff --git a/src/listeners/message/automodCreate.ts b/src/listeners/message/automodCreate.ts index 94b73c7..ae1bd21 100644 --- a/src/listeners/message/automodCreate.ts +++ b/src/listeners/message/automodCreate.ts @@ -5,7 +5,7 @@ import _badLinks from '@root/lib/badlinks'; // Stolen from https://github.com/na import _badLinksSecret from '@root/lib/badlinks-secret'; // shhhh // @ts-expect-error: ts doesn't recognize json5 import badWords from '@root/lib/badwords'; -import { MessageEmbed, TextChannel } from 'discord.js'; +import { MessageEmbed } from 'discord.js'; import { BushClientEvents } from '../../lib/extensions/discord.js/BushClientEvents'; export default class AutomodMessageCreateListener extends BushListener { @@ -100,14 +100,11 @@ export default class AutomodMessageCreateListener extends BushListener { ? util.colors.orange : util.colors.red; - const automodChannel = (await message.guild.getSetting('logChannels')).automod; + const automodChannel = await message.guild.getLogChannel('automod'); if (!automodChannel) return; - const fetchedChannel = (message.guild.channels.cache.get(automodChannel) ?? - (await message.guild.channels.fetch(automodChannel).catch(() => null))) as TextChannel; - if (!fetchedChannel) return; - if (fetchedChannel.permissionsFor(message.guild.me!.id)?.has(['VIEW_CHANNEL', 'SEND_MESSAGES', 'EMBED_LINKS'])) - void fetchedChannel.send({ + if (automodChannel.permissionsFor(message.guild.me!.id)?.has(['VIEW_CHANNEL', 'SEND_MESSAGES', 'EMBED_LINKS'])) + void automodChannel.send({ embeds: [ new MessageEmbed() .setTitle(`[Severity ${highestOffence}] Automod Action Performed`) diff --git a/src/listeners/other/promiseRejection.ts b/src/listeners/other/promiseRejection.ts index 8785b78..130daa3 100644 --- a/src/listeners/other/promiseRejection.ts +++ b/src/listeners/other/promiseRejection.ts @@ -12,8 +12,9 @@ export default class PromiseRejectionListener extends BushListener { public override async exec(error: Error): Promise<void> { // eslint-disable-next-line @typescript-eslint/no-base-to-string void client.console.error('promiseRejection', `An unhanded promise rejection occurred:\n${error.stack ?? error}`, false); - void client.console.channelError({ - embeds: [await CommandErrorListener.generateErrorEmbed({ type: 'unhandledRejection', error: error })] - }); + if (!error.message.includes('reason: getaddrinfo ENOTFOUND canary.discord.com')) + void client.console.channelError({ + embeds: [await CommandErrorListener.generateErrorEmbed({ type: 'unhandledRejection', error: error })] + }); } } diff --git a/src/tasks/removeExpiredPunishements.ts b/src/tasks/removeExpiredPunishements.ts index 49267f5..0610718 100644 --- a/src/tasks/removeExpiredPunishements.ts +++ b/src/tasks/removeExpiredPunishements.ts @@ -33,7 +33,7 @@ export default class RemoveExpiredPunishmentsTask extends BushTask { switch (entry.type) { case ActivePunishmentType.BAN: { if (!user) throw new Error(`user is undefined`); - const result = await guild.unban({ user: user, reason: 'Punishment expired.' }); + const result = await guild.bushUnban({ user: user, reason: 'Punishment expired.' }); if (['success', 'user not banned'].includes(result)) await entry.destroy(); else throw new Error(result); void client.logger.verbose(`removeExpiredPunishments`, `Unbanned ${entry.user}.`); |