From 602329510e11165aef42b3c6071bb1118e4d95c3 Mon Sep 17 00:00:00 2001 From: IRONM00N <64110067+IRONM00N@users.noreply.github.com> Date: Wed, 29 Dec 2021 17:17:39 -0500 Subject: lockdown and unlockdown command --- src/lib/common/ConfirmationPrompt.ts | 90 +++++++++++++++++++++++ src/lib/extensions/discord.js/BushGuild.ts | 111 ++++++++++++++++++++++++++++- src/lib/index.ts | 7 +- 3 files changed, 203 insertions(+), 5 deletions(-) create mode 100644 src/lib/common/ConfirmationPrompt.ts (limited to 'src/lib') diff --git a/src/lib/common/ConfirmationPrompt.ts b/src/lib/common/ConfirmationPrompt.ts new file mode 100644 index 0000000..9e790c7 --- /dev/null +++ b/src/lib/common/ConfirmationPrompt.ts @@ -0,0 +1,90 @@ +import { type BushMessage, type BushSlashMessage } from '#lib'; +import { MessageActionRow, MessageButton, type MessageComponentInteraction, type MessageOptions } from 'discord.js'; +import { MessageButtonStyles } from 'discord.js/typings/enums'; + +/** + * Sends a message with buttons for the user to confirm or cancel the action. + */ +export class ConfirmationPrompt { + /** + * Options for sending the message + */ + protected messageOptions: MessageOptions; + + /** + * The message that triggered the command + */ + protected message: BushMessage | BushSlashMessage; + + /** + * @param message The message to respond to + * @param options The send message options + */ + protected constructor(message: BushMessage | BushSlashMessage, messageOptions: MessageOptions) { + this.message = message; + this.messageOptions = messageOptions; + } + + /** + * Sends a message with buttons for the user to confirm or cancel the action. + */ + protected async send(): Promise { + this.messageOptions.components = [ + new MessageActionRow().addComponents( + new MessageButton({ + style: MessageButtonStyles.SUCCESS, + customId: 'confirmationPrompt__confirm', + emoji: util.emojis.successFull, + label: 'Yes' + }), + new MessageButton({ + style: MessageButtonStyles.DANGER, + customId: 'confirmationPrompt__deny', + emoji: util.emojis.errorFull, + label: 'No' + }) + ) + ]; + + const msg = (await this.message.util.reply(this.messageOptions)) as BushMessage; + + return await new Promise((resolve) => { + let responded = false; + const collector = msg.createMessageComponentCollector({ + filter: (interaction) => + ['confirmationPrompt__confirm', 'confirmationPrompt__deny'].includes(interaction.customId) && + interaction.message?.id == msg.id, + time: 300000 + }); + + collector.on('collect', async (interaction: MessageComponentInteraction) => { + await interaction.deferUpdate().catch(() => undefined); + if (interaction.user.id == this.message.author.id || client.config.owners.includes(interaction.user.id)) { + if (interaction.id === 'confirmationPrompt__confirm') { + resolve(true); + responded = true; + collector.stop(); + } else if (interaction.id === 'confirmationPrompt__deny') { + resolve(false); + responded = true; + collector.stop(); + } + } + }); + + collector.on('end', async () => { + await msg.delete().catch(() => undefined); + if (!responded) resolve(false); + }); + }); + } + + /** + * Sends a message with buttons for the user to confirm or cancel the action. + * @param message The message to respond to + * @param options The send message options + */ + public static async send(message: BushMessage | BushSlashMessage, sendOptions: MessageOptions): Promise { + return new ConfirmationPrompt(message, sendOptions).send(); + } +} diff --git a/src/lib/extensions/discord.js/BushGuild.ts b/src/lib/extensions/discord.js/BushGuild.ts index e12053e..299fdbc 100644 --- a/src/lib/extensions/discord.js/BushGuild.ts +++ b/src/lib/extensions/discord.js/BushGuild.ts @@ -2,29 +2,41 @@ import type { BushClient, BushGuildMember, BushGuildMemberManager, + BushGuildMemberResolvable, + BushNewsChannel, BushTextChannel, + BushThreadChannel, BushUser, BushUserResolvable, GuildFeatures, GuildLogType, GuildModel } from '#lib'; -import { Guild, MessagePayload, type MessageOptions, type UserResolvable } from 'discord.js'; +import { + Collection, + Guild, + GuildChannelManager, + Snowflake, + type MessageOptions, + type MessagePayload, + type UserResolvable +} from 'discord.js'; import type { RawGuildData } from 'discord.js/typings/rawDataTypes'; import _ from 'lodash'; -import { Moderation } from '../../common/Moderation.js'; +import { Moderation } from '../../common/util/Moderation.js'; import { Guild as GuildDB } from '../../models/Guild.js'; import { ModLogType } from '../../models/ModLog.js'; /** * Represents a guild (or a server) on Discord. * It's recommended to see if a guild is available before performing operations or reading data from it. You can - * check this with {@link Guild.available}. + * check this with {@link BushGuild.available}. */ export class BushGuild extends Guild { public declare readonly client: BushClient; public declare readonly me: BushGuildMember | null; public declare members: BushGuildMemberManager; + public declare channels: GuildChannelManager; public constructor(client: BushClient, data: RawGuildData) { super(client, data); @@ -261,6 +273,58 @@ export class BushGuild extends Guild { void client.console.info(_.camelCase(title), message.replace(/\*\*(.*?)\*\*/g, '<<$1>>')); void this.sendLogChannel('error', { embeds: [{ title: title, description: message, color: util.colors.error }] }); } + + /** + * Denies send permissions in specified channels + * @param options The options for locking down the guild + */ + public async lockdown(options: LockdownOptions): Promise { + if (!options.all && !options.channel) return 'all not chosen and no channel specified'; + const channelIds = options.all ? await this.getSetting('lockdownChannels') : [options.channel!.id]; + + if (!channelIds.length) return 'no channels configured'; + const mappedChannels = channelIds.map((id) => this.channels.cache.get(id)); + + const invalidChannels = mappedChannels.filter((c) => c === undefined); + if (invalidChannels.length) return `invalid channel configured: ${invalidChannels.join(', ')}`; + + const moderator = this.members.resolve(options.moderator); + if (!moderator) return 'moderator not found'; + const errors = new Collection(); + let successCount = 0; + + for (const _channel of mappedChannels) { + const channel = _channel!; + if (!channel.permissionsFor(this.me!.id)?.has(['MANAGE_CHANNELS'])) { + errors.set(channel.id, new Error('client no permission')); + continue; + } else if (!channel.permissionsFor(options.moderator)?.has(['MANAGE_CHANNELS'])) { + errors.set(channel.id, new Error('moderator no permission')); + continue; + } + + const reason = `${options.unlock ? 'Unlocking' : 'Locking Down'} Channel | ${moderator.user.tag} | ${ + options.reason ?? 'No reason provided' + }`; + + if (channel.isThread()) { + const lockdownSuccess = await channel.parent?.permissionOverwrites + .edit(this.id, { SEND_MESSAGES_IN_THREADS: options.unlock ? null : false }, { reason }) + .catch((e) => e); + if (lockdownSuccess instanceof Error) errors.set(channel.id, lockdownSuccess); + else successCount++; + } else { + const lockdownSuccess = await channel.permissionOverwrites + .edit(this.id, { SEND_MESSAGES: options.unlock ? null : false }, { reason }) + .catch((e) => e); + if (lockdownSuccess instanceof Error) errors.set(channel.id, lockdownSuccess); + else successCount++; + } + } + + if (errors.size) return errors; + else return `success: ${successCount}`; + } } /** @@ -329,3 +393,44 @@ type BanResponse = PunishmentResponse | 'error banning' | 'error creating ban en * Response returned when unbanning a user */ type UnbanResponse = PunishmentResponse | 'user not banned' | 'error unbanning' | 'error removing ban entry'; + +/** + * Options for locking down channel(s) + */ +interface LockdownOptions { + /** + * The moderator responsible for the lockdown + */ + moderator: BushGuildMemberResolvable; + + /** + * Whether to lock down all (specified) channels + */ + all: boolean; + + /** + * Reason for the lockdown + */ + reason?: string; + + /** + * A specific channel to lockdown + */ + channel?: BushThreadChannel | BushNewsChannel | BushTextChannel; + + /** + * Whether or not to unlock the channel(s) instead of locking them + */ + unlock?: boolean; +} + +/** + * Response returned when locking down a channel + */ +type LockdownResponse = + | `success: ${number}` + | 'all not chosen and no channel specified' + | 'no channels configured' + | `invalid channel configured: ${string}` + | 'moderator not found' + | Collection; diff --git a/src/lib/index.ts b/src/lib/index.ts index 94a7dd9..8809e27 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,11 +1,12 @@ export * from './common/AutoMod.js'; export * from './common/ButtonPaginator.js'; +export * from './common/ConfirmationPrompt.js'; export * from './common/DeleteButton.js'; -export * from './common/Format.js'; -export * from './common/Moderation.js'; export type { BushInspectOptions } from './common/typings/BushInspectOptions.js'; export type { CodeBlockLang } from './common/typings/CodeBlockLang.js'; export * from './common/util/Arg.js'; +export * from './common/util/Format.js'; +export * from './common/util/Moderation.js'; export * from './extensions/discord-akairo/BushArgumentTypeCaster.js'; export * from './extensions/discord-akairo/BushClient.js'; export * from './extensions/discord-akairo/BushClientUtil.js'; @@ -38,6 +39,7 @@ export * from './extensions/discord.js/BushGuild.js'; export type { BushGuildApplicationCommandManager } from './extensions/discord.js/BushGuildApplicationCommandManager.js'; export type { BushGuildBan } from './extensions/discord.js/BushGuildBan.js'; export * from './extensions/discord.js/BushGuildChannel.js'; +export type { BushGuildChannelManager } from './extensions/discord.js/BushGuildChannelManager.js'; export * from './extensions/discord.js/BushGuildEmoji.js'; export type { BushGuildEmojiRoleManager } from './extensions/discord.js/BushGuildEmojiRoleManager.js'; export type { BushGuildManager } from './extensions/discord.js/BushGuildManager.js'; @@ -63,6 +65,7 @@ export * from './extensions/discord.js/BushUser.js'; export type { BushUserManager } from './extensions/discord.js/BushUserManager.js'; export * from './extensions/discord.js/BushVoiceChannel.js'; export * from './extensions/discord.js/BushVoiceState.js'; +export * from './extensions/discord.js/other.js'; export * from './models/ActivePunishment.js'; export * from './models/BaseModel.js'; export * from './models/Global.js'; -- cgit