diff options
-rw-r--r-- | src/commands/moderation/massBan.ts | 34 | ||||
-rw-r--r-- | src/lib/common/util/Moderation.ts | 101 | ||||
-rw-r--r-- | src/lib/extensions/discord-akairo/BushClientUtil.ts | 17 | ||||
-rw-r--r-- | src/lib/extensions/discord.js/BushClientEvents.ts | 7 | ||||
-rw-r--r-- | src/lib/extensions/discord.js/BushGuild.ts | 85 | ||||
-rw-r--r-- | src/listeners/member-custom/massBan.ts | 38 |
6 files changed, 234 insertions, 48 deletions
diff --git a/src/commands/moderation/massBan.ts b/src/commands/moderation/massBan.ts index e4aeb55..499bcd4 100644 --- a/src/commands/moderation/massBan.ts +++ b/src/commands/moderation/massBan.ts @@ -1,6 +1,14 @@ -import { BushCommand, type ArgType, type BushMessage, type BushSlashMessage, type OptionalArgType } from '#lib'; +import { + BanResponse, + banResponse, + BushCommand, + type ArgType, + type BushMessage, + type BushSlashMessage, + type OptionalArgType +} from '#lib'; import assert from 'assert'; -import { ApplicationCommandOptionType, Embed, PermissionFlagsBits } from 'discord.js'; +import { ApplicationCommandOptionType, Collection, Embed, PermissionFlagsBits } from 'discord.js'; export default class MassBanCommand extends BushCommand { public constructor() { @@ -8,8 +16,8 @@ export default class MassBanCommand extends BushCommand { aliases: ['mass-ban', 'mass-dban'], category: 'moderation', description: 'Ban multiple users at once.', - usage: ['template <...users> [--reason "<reason>"] [--days <days>]'], - examples: ['template 1 2'], + usage: ['mass-ban <...users> [--reason "<reason>"] [--days <days>]'], + examples: ['mass-ban 311294982898057217 792202575851814942 792199864510447666 792201010118131713 --reason "too many alts"'], args: [ { id: 'users', @@ -71,21 +79,29 @@ export default class MassBanCommand extends BushCommand { } const promises = ids.map((id) => - message.guild.bushBan({ + message.guild.massBanOne({ user: id, - reason: `[MassBan] ${args.reason ? args.reason.trim() : 'No reason provided.'}`, moderator: message.author.id, + reason: `[MassBan] ${args.reason ? args.reason.trim() : 'No reason provided.'}`, deleteDays: args.days ?? 0 }) ); - const res = await Promise.allSettled(promises); + const res = await Promise.all(promises); + + const map = new Collection(res.map((r, i) => [ids[i], r])); + client.emit('massBan', message.member!, message.guild!, args.reason ? args.reason.trim() : 'No reason provided.', map); + + const success = (res: BanResponse): boolean => [banResponse.SUCCESS, banResponse.DM_ERROR].includes(res as any); const embed = new Embed() .setTitle(`Mass Ban Results`) .setDescription( - res.map((r, i) => `${r.status === 'rejected' ? util.emojis.error : util.emojis.success} ${ids[i]}`).join('') - ); + res + .map((r, i) => `${success(r) ? util.emojis.success : util.emojis.error} ${ids[i]}${success(r) ? '' : ` - ${r}`}`) + .join('\n') + ) + .setColor(util.colors.DarkRed); return message.util.send({ embeds: [embed] }); } diff --git a/src/lib/common/util/Moderation.ts b/src/lib/common/util/Moderation.ts index 04ccb31..365dbd5 100644 --- a/src/lib/common/util/Moderation.ts +++ b/src/lib/common/util/Moderation.ts @@ -123,25 +123,41 @@ export class Moderation { const user = (await util.resolveNonCachedUser(options.user))!.id; const moderator = (await util.resolveNonCachedUser(options.moderator))!.id; const guild = client.guilds.resolveId(options.guild)!; - const duration = options.duration ? options.duration : undefined; + return this.createModLogEntrySimple( + { + ...options, + user: user, + moderator: moderator, + guild: guild + }, + getCaseNumber + ); + } + + /** + * 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 Guild.findOrCreate({ - where: { - id: guild - }, - defaults: { - id: guild - } + where: { id: options.guild }, + defaults: { id: options.guild } }); const modLogEntry = ModLog.build({ type: options.type, - user, - moderator, + user: options.user, + moderator: options.moderator, reason: options.reason, - duration: duration, - guild, + duration: options.duration ? options.duration : undefined, + guild: options.guild, pseudo: options.pseudo ?? false, evidence: options.evidence, hidden: options.hidden ?? false @@ -153,7 +169,9 @@ export class Moderation { if (!getCaseNumber) return { log: saveResult, caseNum: null }; - const caseNum = (await ModLog.findAll({ where: { type: options.type, user: user, guild: guild, hidden: 'false' } }))?.length; + const caseNum = ( + await ModLog.findAll({ where: { type: options.type, user: options.user, guild: options.guild, hidden: false } }) + )?.length; return { log: saveResult, caseNum }; } @@ -269,7 +287,6 @@ export class Moderation { if (appealsEnabled && options.modlog) components = [ new ActionRow({ - type: ComponentType.ActionRow, components: [ new ButtonComponent({ custom_id: `appeal;${this.punishmentToPresentTense(options.punishment)};${ @@ -294,54 +311,76 @@ export class Moderation { } } -/** - * Options for creating a modlog entry. - */ -export interface CreateModLogEntryOptions { +interface BaseCreateModLogEntryOptions { /** * The type of modlog entry. */ type: ModLogType; /** - * The user that a modlog entry is created for. + * The reason for the punishment. */ - user: BushGuildMemberResolvable; + reason: string | undefined | null; /** - * The moderator that created the modlog entry. + * The duration of the punishment. */ - moderator: BushGuildMemberResolvable; + duration?: number; /** - * The reason for the punishment. + * Whether the punishment is a pseudo punishment. */ - reason: string | undefined | null; + pseudo?: boolean; /** - * The duration of the punishment. + * The evidence for the punishment. */ - duration?: number; + evidence?: string; + + /** + * Makes the modlog entry hidden. + */ + hidden?: boolean; +} + +/** + * Options for creating a modlog entry. + */ +export interface CreateModLogEntryOptions extends BaseCreateModLogEntryOptions { + /** + * The user that a modlog entry is created for. + */ + user: BushGuildMemberResolvable; + + /** + * The moderator that created the modlog entry. + */ + moderator: BushGuildMemberResolvable; /** * The guild that the punishment is created for. */ guild: BushGuildResolvable; +} +/** + * Simple options for creating a modlog entry. + */ +export interface SimpleCreateModLogEntryOptions extends BaseCreateModLogEntryOptions { /** - * Whether the punishment is a pseudo punishment. + * The user that a modlog entry is created for. */ - pseudo?: boolean; + user: Snowflake; /** - * The evidence for the punishment. + * The moderator that created the modlog entry. */ - evidence?: string; + moderator: Snowflake; /** - * Makes the modlog entry hidden. + * The guild that the punishment is created for. */ - hidden?: boolean; + guild: Snowflake; } /** diff --git a/src/lib/extensions/discord-akairo/BushClientUtil.ts b/src/lib/extensions/discord-akairo/BushClientUtil.ts index ecfa360..9903140 100644 --- a/src/lib/extensions/discord-akairo/BushClientUtil.ts +++ b/src/lib/extensions/discord-akairo/BushClientUtil.ts @@ -678,16 +678,17 @@ export class BushClientUtil extends ClientUtil { */ public async resolveNonCachedUser(user: UserResolvable | undefined | null): Promise<BushUser | undefined> { if (user == null) return undefined; - const id = - user instanceof User || user instanceof GuildMember || user instanceof ThreadMember - ? user.id + const resolvedUser = + user instanceof User + ? <BushUser>user + : user instanceof GuildMember + ? <BushUser>user.user + : user instanceof ThreadMember + ? <BushUser>user.user : user instanceof Message - ? user.author.id - : typeof user === 'string' - ? user + ? <BushUser>user.author : undefined; - if (!id) return undefined; - else return await client.users.fetch(id).catch(() => undefined); + return resolvedUser ?? (await client.users.fetch(user as Snowflake).catch(() => undefined)); } /** diff --git a/src/lib/extensions/discord.js/BushClientEvents.ts b/src/lib/extensions/discord.js/BushClientEvents.ts index 50b198d..e6cf93f 100644 --- a/src/lib/extensions/discord.js/BushClientEvents.ts +++ b/src/lib/extensions/discord.js/BushClientEvents.ts @@ -1,4 +1,5 @@ import type { + BanResponse, BushApplicationCommand, BushClient, BushDMChannel, @@ -264,6 +265,12 @@ export interface BushClientEvents extends AkairoClientEvents { channelsSuccessMap: Collection<Snowflake, boolean>, all?: boolean ]; + massBan: [ + moderator: BushGuildMember, + guild: BushGuild, + reason: string | undefined, + results: Collection<Snowflake, BanResponse> + ]; } type Setting = GuildSettings | 'enabledFeatures' | 'blacklistedChannels' | 'blacklistedUsers' | 'disabledCommands'; diff --git a/src/lib/extensions/discord.js/BushGuild.ts b/src/lib/extensions/discord.js/BushGuild.ts index 80799fd..155f32c 100644 --- a/src/lib/extensions/discord.js/BushGuild.ts +++ b/src/lib/extensions/discord.js/BushGuild.ts @@ -236,6 +236,69 @@ export class BushGuild extends Guild { } /** + * {@link bushBan} with less resolving and checks + * @param options Options for banning the user. + * @returns A string status message of the ban. + * **Preconditions:** + * - {@link me} has the `BanMembers` permission + * **Warning:** + * - Doesn't emit bushBan Event + */ + public async massBanOne(options: GuildMassBanOneOptions): Promise<BanResponse> { + if (this.bans.cache.has(options.user)) return banResponse.ALREADY_BANNED; + + const ret = await (async () => { + // add modlog entry + const { log: modlog } = await Moderation.createModLogEntrySimple({ + type: ModLogType.PERM_BAN, + user: options.user, + moderator: options.moderator, + reason: options.reason, + duration: 0, + guild: this.id + }); + if (!modlog) return banResponse.MODLOG_ERROR; + + let dmSuccessEvent: boolean | undefined = undefined; + // dm user + if (this.members.cache.has(options.user)) { + dmSuccessEvent = await Moderation.punishDM({ + modlog: modlog.id, + guild: this, + user: options.user, + punishment: 'banned', + duration: 0, + reason: options.reason ?? undefined, + sendFooter: true + }); + } + + // ban + const banSuccess = await this.bans + .create(options.user, { + reason: `${options.moderator} | ${options.reason}`, + deleteMessageDays: options.deleteDays + }) + .catch(() => false); + if (!banSuccess) return banResponse.ACTION_ERROR; + + // add punishment entry so they can be unbanned later + const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ + type: 'ban', + user: options.user, + guild: this, + duration: 0, + modlog: modlog.id + }); + if (!punishmentEntrySuccess) return banResponse.PUNISHMENT_ENTRY_ADD_ERROR; + + if (!dmSuccessEvent) return banResponse.DM_ERROR; + return banResponse.SUCCESS; + })(); + return ret; + } + + /** * Unbans a user, dms them, creates a mod log entry, and destroys the punishment entry. * @param options Options for unbanning the user. * @returns A status message of the unban. @@ -414,6 +477,28 @@ export interface GuildBushUnbanOptions { evidence?: string; } +export interface GuildMassBanOneOptions { + /** + * The user to ban + */ + user: Snowflake; + + /** + * The reason to ban the user + */ + reason: string; + + /** + * The moderator who banned the user + */ + moderator: Snowflake; + + /** + * The number of days to delete the user's messages for + */ + deleteDays?: number; +} + /** * Options for banning a user */ diff --git a/src/listeners/member-custom/massBan.ts b/src/listeners/member-custom/massBan.ts new file mode 100644 index 0000000..93089a3 --- /dev/null +++ b/src/listeners/member-custom/massBan.ts @@ -0,0 +1,38 @@ +import { BanResponse, banResponse, BushListener, type BushClientEvents } from '#lib'; +import { Embed } from 'discord.js'; + +export default class BushBanListener extends BushListener { + public constructor() { + super('massBan', { + emitter: 'client', + event: 'massBan', + category: 'member-custom' + }); + } + + public override async exec(...[moderator, guild, reason, results]: BushClientEvents['massBan']) { + const logChannel = await guild.getLogChannel('moderation'); + if (!logChannel) return; + + const success = (res: BanResponse): boolean => [banResponse.SUCCESS, banResponse.DM_ERROR].includes(res as any); + + const logEmbed = new Embed() + .setColor(util.colors.DarkRed) + .setTimestamp() + .setTitle('Mass Ban') + .addFields( + { name: '**Moderator**', value: `${moderator} (${moderator.user.tag})` }, + { name: '**Reason**', value: `${reason ? reason : '[No Reason Provided]'}` } + ) + .setDescription( + results + .map( + (reason, user) => + `${success(reason) ? util.emojis.success : util.emojis.error} ${user}${success(reason) ? '' : ` - ${reason}`}` + ) + .join('\n') + ); + + return await logChannel.send({ embeds: [logEmbed] }); + } +} |