aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/commands/moderation/_lockdown.ts44
-rw-r--r--src/commands/moderation/lockdown.ts144
-rw-r--r--src/commands/moderation/unlockdown.ts58
-rw-r--r--src/lib/common/ConfirmationPrompt.ts90
-rw-r--r--src/lib/extensions/discord.js/BushGuild.ts111
-rw-r--r--src/lib/index.ts7
-rw-r--r--src/listeners/client/akairoDebug.ts4
7 files changed, 407 insertions, 51 deletions
diff --git a/src/commands/moderation/_lockdown.ts b/src/commands/moderation/_lockdown.ts
deleted file mode 100644
index 08d4011..0000000
--- a/src/commands/moderation/_lockdown.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { BushCommand, type BushMessage, type BushNewsChannel, type BushSlashMessage, type BushTextChannel } from '#lib';
-
-export default class LockdownCommand extends BushCommand {
- public constructor() {
- super('lockdown', {
- aliases: ['lockdown', 'unlockdown'],
- category: 'moderation',
- description: 'Allows you to lockdown a channel or all configured channels..',
- usage: ['lockdown [--all]'],
- examples: ['lockdown', 'lockdown --all'],
- args: [
- {
- id: 'all',
- description: 'Whether or not to lock all channels',
- match: 'flag',
- flag: '--all',
- prompt: 'Would you like to lockdown all channels?',
- slashType: 'BOOLEAN',
- optional: true
- }
- ],
- slash: true,
- channel: 'guild',
- clientPermissions: (m) => util.clientSendAndPermCheck(m),
- userPermissions: [],
- hidden: true
- });
- }
-
- public override async exec(message: BushMessage | BushSlashMessage, args: { all: boolean }) {
- // todo stop being lazy
- return await message.util.reply('Unfortunately my developer is too lazy to implement this command.');
- if (!args.all) {
- if (!['GUILD_TEXT', 'GUILD_NEWS'].includes(message.channel!.type))
- return message.util.reply(`${util.emojis.error} You can only lock down text and announcement channels.`);
-
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const lockdownSuccess = await util.lockdownChannel({
- channel: message.channel as BushTextChannel | BushNewsChannel,
- moderator: message.author
- });
- }
- }
-}
diff --git a/src/commands/moderation/lockdown.ts b/src/commands/moderation/lockdown.ts
new file mode 100644
index 0000000..7a0b9d8
--- /dev/null
+++ b/src/commands/moderation/lockdown.ts
@@ -0,0 +1,144 @@
+import {
+ AllowedMentions,
+ BushCommand,
+ BushNewsChannel,
+ BushTextChannel,
+ BushThreadChannel,
+ ConfirmationPrompt,
+ type ArgType,
+ type BushMessage,
+ type BushSlashMessage,
+ type OptionalArgType
+} from '#lib';
+import assert from 'assert';
+import { Collection } from 'discord.js';
+
+export default class LockdownCommand extends BushCommand {
+ public constructor() {
+ super('lockdown', {
+ aliases: ['lockdown'],
+ category: 'moderation',
+ description: 'Allows you to lockdown a channel or all configured channels.',
+ usage: ['lockdown [channel] [reason] [--all]'],
+ examples: ['lockdown', 'lockdown --all'],
+ args: [
+ {
+ id: 'channel',
+ description: 'Specify a different channel to lockdown instead of the one you trigger the command in.',
+ type: util.arg.union('textChannel', 'newsChannel', 'threadChannel'),
+ prompt: 'What channel would you like to lockdown?',
+ slashType: 'CHANNEL',
+ channelTypes: ['GUILD_TEXT', 'GUILD_NEWS', 'GUILD_NEWS_THREAD', 'GUILD_PUBLIC_THREAD', 'GUILD_PRIVATE_THREAD'],
+ optional: true
+ },
+ {
+ id: 'all',
+ description: 'Whether or not to lock all configured channels.',
+ match: 'flag',
+ flag: '--all',
+ prompt: 'Would you like to lockdown all configured channels?',
+ slashType: 'BOOLEAN',
+ optional: true
+ },
+ {
+ id: 'reason',
+ description: 'The reason for the lockdown.',
+ type: 'string',
+ match: 'rest',
+ prompt: 'What is the reason for the lockdown?',
+ slashType: 'STRING',
+ optional: true
+ }
+ ],
+ slash: true,
+ channel: 'guild',
+ clientPermissions: (m) => util.clientSendAndPermCheck(m, ['MANAGE_CHANNELS']),
+ userPermissions: ['MANAGE_CHANNELS']
+ });
+ }
+
+ public override async exec(
+ message: BushMessage | BushSlashMessage,
+ args: {
+ channel: OptionalArgType<'textChannel'> | OptionalArgType<'newsChannel'> | OptionalArgType<'threadChannel'>;
+ reason: OptionalArgType<'string'>;
+ all: ArgType<'boolean'>;
+ }
+ ) {
+ client.console.debug('lockdown command');
+ return await LockdownCommand.lockdownOrUnlockdown(message, args, 'lockdown');
+ }
+
+ public static async lockdownOrUnlockdown(
+ message: BushMessage | BushSlashMessage,
+ args: {
+ channel: OptionalArgType<'textChannel'> | OptionalArgType<'newsChannel'> | OptionalArgType<'threadChannel'>;
+ reason: OptionalArgType<'string'>;
+ all: ArgType<'boolean'>;
+ },
+ action: 'lockdown' | 'unlockdown'
+ ) {
+ assert(message.inGuild());
+
+ if (args.channel && args.all)
+ return await message.util.reply(`${util.emojis.error} You can't specify a channel and set all to true at the same time.`);
+
+ const channel = args.channel ?? message.channel;
+
+ if (!(channel instanceof BushTextChannel || channel instanceof BushNewsChannel || channel instanceof BushThreadChannel))
+ return await message.util.reply(
+ `${util.emojis.error} You can only ${action} text channels, news channels, and thread channels.`
+ );
+
+ if (args.all) {
+ const confirmation = await ConfirmationPrompt.send(message, {
+ content: `Are you sure you want to ${action} all channels?`
+ });
+ if (!confirmation) return message.util.send(`${util.emojis.error} Lockdown cancelled.`);
+ }
+
+ client.console.debug('right before lockdown');
+ const response = await message.guild.lockdown({
+ moderator: message.author,
+ channel: channel ?? undefined,
+ reason: args.reason ?? undefined,
+ all: args.all ?? false,
+ unlock: action === 'unlockdown'
+ });
+
+ if (response instanceof Collection) {
+ return await message.util.send({
+ content: `${util.emojis.error} The following channels failed to ${action}:`,
+ embeds: [
+ {
+ description: response.map((e, c) => `<#${c}> : ${e.message}`).join('\n'),
+ color: util.colors.warn
+ }
+ ]
+ });
+ } else {
+ let messageResponse;
+ if (response === 'all not chosen and no channel specified') {
+ messageResponse = `${util.emojis.error} You must specify a channel to ${action}.`;
+ } else if (response.startsWith('invalid channel configured: ')) {
+ const channels = response.replace('invalid channel configured: ', '');
+ const actionFormatted = `${action.replace('down', '')}ed`;
+ messageResponse = `${util.emojis.error} Some of the channels configured to be ${actionFormatted} cannot be resolved: ${channels}}`;
+ } else if (response === 'no channels configured') {
+ messageResponse = `${util.emojis.error} The all option is selected but there are no channels configured to be locked down.`;
+ } else if (response === 'moderator not found') {
+ messageResponse = `${util.emojis.error} For some reason I could not resolve you?`;
+ } else if (response.startsWith('success: ')) {
+ const num = Number.parseInt(response.replace('success: ', ''));
+ messageResponse = `${util.emojis.success} Successfully ${
+ action === 'lockdown' ? 'locked down' : 'unlocked'
+ } **${num}** channel${num > 0 ? 's' : ''}.`;
+ } else {
+ throw new Error(`Unknown response: ${response}`);
+ }
+
+ assert(messageResponse);
+ return await message.util.send({ content: messageResponse, allowedMentions: AllowedMentions.none() });
+ }
+ }
+}
diff --git a/src/commands/moderation/unlockdown.ts b/src/commands/moderation/unlockdown.ts
new file mode 100644
index 0000000..87b27eb
--- /dev/null
+++ b/src/commands/moderation/unlockdown.ts
@@ -0,0 +1,58 @@
+import { BushCommand, type ArgType, type BushMessage, type BushSlashMessage, type OptionalArgType } from '#lib';
+import LockdownCommand from './lockdown.js';
+
+export default class UnlockdownCommand extends BushCommand {
+ public constructor() {
+ super('unlockdown', {
+ aliases: ['unlockdown'],
+ category: 'moderation',
+ description: 'Allows you to unlockdown a channel or all configured channels.',
+ usage: ['unlockdown [channel] [reason] [--all]'],
+ examples: ['unlockdown', 'unlockdown --all'],
+ args: [
+ {
+ id: 'channel',
+ description: 'Specify a different channel to unlockdown instead of the one you trigger the command in.',
+ type: util.arg.union('textChannel', 'newsChannel', 'threadChannel'),
+ prompt: 'What channel would you like to unlockdown?',
+ slashType: 'CHANNEL',
+ channelTypes: ['GUILD_TEXT', 'GUILD_NEWS', 'GUILD_NEWS_THREAD', 'GUILD_PUBLIC_THREAD', 'GUILD_PRIVATE_THREAD'],
+ optional: true
+ },
+ {
+ id: 'all',
+ description: 'Whether or not to unlock all configured channels.',
+ match: 'flag',
+ flag: '--all',
+ prompt: 'Would you like to unlockdown all configured channels?',
+ slashType: 'BOOLEAN',
+ optional: true
+ },
+ {
+ id: 'reason',
+ description: 'The reason for the unlock.',
+ type: 'string',
+ match: 'rest',
+ prompt: 'What is the reason for the unlock?',
+ slashType: 'STRING',
+ optional: true
+ }
+ ],
+ slash: true,
+ channel: 'guild',
+ clientPermissions: (m) => util.clientSendAndPermCheck(m, ['MANAGE_CHANNELS']),
+ userPermissions: ['MANAGE_CHANNELS']
+ });
+ }
+
+ public override async exec(
+ message: BushMessage | BushSlashMessage,
+ args: {
+ channel: OptionalArgType<'textChannel'> | OptionalArgType<'newsChannel'> | OptionalArgType<'threadChannel'>;
+ reason: OptionalArgType<'string'>;
+ all: ArgType<'boolean'>;
+ }
+ ) {
+ return await LockdownCommand.lockdownOrUnlockdown(message, args, 'unlockdown');
+ }
+}
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<boolean> {
+ 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<boolean>((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<boolean> {
+ 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.
* <info>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}.</info>
+ * check this with {@link BushGuild.available}.</info>
*/
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<LockdownResponse> {
+ 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<Snowflake, Error>();
+ 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<string, Error>;
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';
diff --git a/src/listeners/client/akairoDebug.ts b/src/listeners/client/akairoDebug.ts
index af66677..c8a28c4 100644
--- a/src/listeners/client/akairoDebug.ts
+++ b/src/listeners/client/akairoDebug.ts
@@ -9,7 +9,7 @@ export default class DiscordJsDebugListener extends BushListener {
});
}
- public override async exec(...[message, ...other]: BushClientEvents['debug']): Promise<void> {
- void client.console.superVerboseRaw('akairoDebug', message, ...other);
+ public override async exec(...[message, ..._other]: BushClientEvents['debug']): Promise<void> {
+ void client.console.superVerboseRaw('akairoDebug', message);
}
}