diff options
author | IRONM00N <64110067+IRONM00N@users.noreply.github.com> | 2021-12-29 22:12:02 -0500 |
---|---|---|
committer | IRONM00N <64110067+IRONM00N@users.noreply.github.com> | 2021-12-29 22:12:02 -0500 |
commit | a540b4083b7a5662ec49b7576b215fe5d3153358 (patch) | |
tree | 8383d3a1337eabfd69e30fb95687691b6e6cc7f7 /src | |
parent | 602329510e11165aef42b3c6071bb1118e4d95c3 (diff) | |
download | tanzanite-a540b4083b7a5662ec49b7576b215fe5d3153358.tar.gz tanzanite-a540b4083b7a5662ec49b7576b215fe5d3153358.tar.bz2 tanzanite-a540b4083b7a5662ec49b7576b215fe5d3153358.zip |
block and unblock command
Diffstat (limited to 'src')
-rw-r--r-- | src/commands/moderation/_block.ts | 0 | ||||
-rw-r--r-- | src/commands/moderation/_unblock.ts | 0 | ||||
-rw-r--r-- | src/commands/moderation/block.ts | 122 | ||||
-rw-r--r-- | src/commands/moderation/unblock.ts | 108 | ||||
-rw-r--r-- | src/lib/extensions/discord.js/BushGuildMember.ts | 267 |
5 files changed, 470 insertions, 27 deletions
diff --git a/src/commands/moderation/_block.ts b/src/commands/moderation/_block.ts deleted file mode 100644 index e69de29..0000000 --- a/src/commands/moderation/_block.ts +++ /dev/null diff --git a/src/commands/moderation/_unblock.ts b/src/commands/moderation/_unblock.ts deleted file mode 100644 index e69de29..0000000 --- a/src/commands/moderation/_unblock.ts +++ /dev/null diff --git a/src/commands/moderation/block.ts b/src/commands/moderation/block.ts new file mode 100644 index 0000000..371975b --- /dev/null +++ b/src/commands/moderation/block.ts @@ -0,0 +1,122 @@ +import { + AllowedMentions, + BushCommand, + BushTextChannel, + BushThreadChannel, + Moderation, + type ArgType, + type BushMessage, + type BushSlashMessage, + type OptionalArgType +} from '#lib'; +import assert from 'assert'; + +export default class BlockCommand extends BushCommand { + public constructor() { + super('block', { + aliases: ['block'], + category: 'moderation', + description: 'Prevent a user from using a channel.', + usage: ['block <member> [reason] [duration]'], + examples: ['block IRONM00N 2h bad jokes'], + args: [ + { + id: 'user', + description: 'The user to block.', + type: 'user', + prompt: 'What user would you like to block?', + retry: '{error} Choose a valid user to block.', + slashType: 'USER' + }, + { + id: 'reason', + description: 'The reason and duration of the block.', + type: 'contentWithDuration', + match: 'rest', + prompt: 'Why should this user be blocked and for how long?', + retry: '{error} Choose a valid block reason and duration.', + optional: true, + slashType: 'STRING' + }, + { + id: 'force', + description: 'Override permission checks.', + flag: '--force', + match: 'flag', + optional: true, + slashType: false, + only: 'text', + ownerOnly: true + } + ], + slash: true, + channel: 'guild', + clientPermissions: (m) => util.clientSendAndPermCheck(m, 'MANAGE_CHANNELS'), + userPermissions: (m) => util.userGuildPermCheck(m, ['MANAGE_MESSAGES']) + }); + } + + public override async exec( + message: BushMessage | BushSlashMessage, + args: { user: ArgType<'user'>; reason: OptionalArgType<'contentWithDuration'> | string; force?: ArgType<'boolean'> } + ) { + assert(message.inGuild()); + if (!(message.channel instanceof BushTextChannel || message.channel instanceof BushThreadChannel)) + return message.util.send(`${util.emojis.error} This command can only be used in text and thread channels.`); + + const reason = args.reason + ? typeof args.reason === 'string' + ? await util.arg.cast('contentWithDuration', message, args.reason) + : args.reason + : { duration: null, contentWithoutTime: '' }; + + if (reason.duration === null) reason.duration = 0; + const member = await message.guild!.members.fetch(args.user.id).catch(() => null); + if (!member) + return await message.util.reply(`${util.emojis.error} The user you selected is not in the server or is not a valid user.`); + + assert(message.member); + const useForce = args.force && message.author.isOwner(); + const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'block', true, useForce); + + if (canModerateResponse !== true) { + return message.util.reply(canModerateResponse); + } + + const time = reason + ? typeof reason === 'string' + ? ((await util.arg.cast('duration', message, reason)) as number) + : reason.duration + : undefined; + + const parsedReason = reason?.contentWithoutTime ?? ''; + + const responseCode = await member.block({ + reason: parsedReason, + moderator: message.member, + duration: time ?? 0, + channel: message.channel + }); + + const responseMessage = () => { + const victim = util.format.input(member.user.tag); + switch (responseCode) { + case 'missing permissions': + return `${util.emojis.error} Could not block ${victim} because I am missing the **Manage Channel** permission.`; + case 'invalid channel': + return `${util.emojis.error} Could not block ${victim}, you can only block users in text or thread channels.`; + case 'error blocking': + return `${util.emojis.error} An unknown error occurred while trying to block ${victim}.`; + case 'error creating modlog entry': + return `${util.emojis.error} There was an error creating a modlog entry, please report this to my developers.`; + case 'error creating block entry': + return `${util.emojis.error} There was an error creating a punishment entry, please report this to my developers.`; + case 'failed to dm': + return `${util.emojis.warn} Blocked ${victim} however I could not send them a dm.`; + case 'success': + return `${util.emojis.success} Successfully blocked ${victim}.`; + } + }; + return await message.util.reply({ content: responseMessage(), allowedMentions: AllowedMentions.none() }); + } +} diff --git a/src/commands/moderation/unblock.ts b/src/commands/moderation/unblock.ts new file mode 100644 index 0000000..f9f069b --- /dev/null +++ b/src/commands/moderation/unblock.ts @@ -0,0 +1,108 @@ +import { + AllowedMentions, + BushCommand, + BushTextChannel, + BushThreadChannel, + Moderation, + type ArgType, + type BushMessage, + type BushSlashMessage, + type OptionalArgType +} from '#lib'; +import assert from 'assert'; + +export default class UnblockCommand extends BushCommand { + public constructor() { + super('unblock', { + aliases: ['unblock'], + category: 'moderation', + description: 'Allows a user to use a channel.', + usage: ['unblock <member> [reason]'], + examples: ['unblock IRONM00N 2h bad jokes'], + args: [ + { + id: 'user', + description: 'The user to unblock.', + type: 'user', + prompt: 'What user would you like to unblock?', + retry: '{error} Choose a valid user to unblock.', + slashType: 'USER' + }, + { + id: 'reason', + description: 'The reason and duration of the unblock.', + type: 'string', + match: 'rest', + prompt: 'Why should this user be blocked and for how long?', + retry: '{error} Choose a valid block reason and duration.', + optional: true, + slashType: 'STRING' + }, + { + id: 'force', + description: 'Override permission checks.', + flag: '--force', + match: 'flag', + optional: true, + slashType: false, + only: 'text', + ownerOnly: true + } + ], + slash: true, + channel: 'guild', + clientPermissions: (m) => util.clientSendAndPermCheck(m, 'MANAGE_CHANNELS'), + userPermissions: (m) => util.userGuildPermCheck(m, ['MANAGE_MESSAGES']) + }); + } + + public override async exec( + message: BushMessage | BushSlashMessage, + args: { user: ArgType<'user'>; reason: OptionalArgType<'string'>; force?: ArgType<'boolean'> } + ) { + assert(message.inGuild()); + if (!(message.channel instanceof BushTextChannel || message.channel instanceof BushThreadChannel)) + return message.util.send(`${util.emojis.error} This command can only be used in text and thread channels.`); + + const member = await message.guild!.members.fetch(args.user.id).catch(() => null); + if (!member) + return await message.util.reply(`${util.emojis.error} The user you selected is not in the server or is not a valid user.`); + + assert(message.member); + const useForce = args.force && message.author.isOwner(); + const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'unblock', true, useForce); + + if (canModerateResponse !== true) { + return message.util.reply(canModerateResponse); + } + + const parsedReason = args.reason ?? ''; + + const responseCode = await member.unblock({ + reason: parsedReason, + moderator: message.member, + channel: message.channel + }); + + const responseMessage = () => { + const victim = util.format.input(member.user.tag); + switch (responseCode) { + case 'missing permissions': + return `${util.emojis.error} Could not unblock ${victim} because I am missing the **Manage Channel** permission.`; + case 'invalid channel': + return `${util.emojis.error} Could not unblock ${victim}, you can only unblock users in text or thread channels.`; + case 'error unblocking': + return `${util.emojis.error} An unknown error occurred while trying to unblock ${victim}.`; + case 'error creating modlog entry': + return `${util.emojis.error} There was an error creating a modlog entry, please report this to my developers.`; + case 'error removing block entry': + return `${util.emojis.error} There was an error creating a punishment entry, please report this to my developers.`; + case 'failed to dm': + return `${util.emojis.warn} Unblocked ${victim} however I could not send them a dm.`; + case 'success': + return `${util.emojis.success} Successfully unblocked ${victim}.`; + } + }; + return await message.util.reply({ content: responseMessage(), allowedMentions: AllowedMentions.none() }); + } +} diff --git a/src/lib/extensions/discord.js/BushGuildMember.ts b/src/lib/extensions/discord.js/BushGuildMember.ts index 1908f12..ac8fb54 100644 --- a/src/lib/extensions/discord.js/BushGuildMember.ts +++ b/src/lib/extensions/discord.js/BushGuildMember.ts @@ -1,7 +1,19 @@ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import { BushClientEvents, Moderation, ModLogType, type BushClient, type BushGuild, type BushRole, type BushUser } from '#lib'; +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { + BushClientEvents, + BushGuildTextBasedChannel, + BushGuildTextChannelResolvable, + BushThreadChannelResolvable, + Moderation, + ModLogType, + type BushClient, + type BushGuild, + type BushRole, + type BushUser +} from '#lib'; import { GuildMember, MessageEmbed, type Partialize, type Role } from 'discord.js'; import type { RawGuildMemberData } from 'discord.js/typings/rawDataTypes'; +/* eslint-enable @typescript-eslint/no-unused-vars */ /** * Represents a member of a guild on Discord. @@ -72,7 +84,7 @@ export class BushGuildMember extends GuildMember { return { result: 'success', caseNum: result.caseNum }; })(); - if (!['error creating modlog entry'].includes(ret.result)) + if (!(['error creating modlog entry'] as const).includes(ret.result)) client.emit('bushWarn', this, moderator, this.guild, options.reason ?? undefined, caseID!, dmSuccessEvent!); return ret as { result: WarnResponse | null; caseNum: number | null }; } @@ -123,7 +135,10 @@ export class BushGuildMember extends GuildMember { return 'success'; })(); - if (!['error adding role', 'error creating modlog entry', 'error creating role entry'].includes(ret) && options.addToModlog) + if ( + !(['error adding role', 'error creating modlog entry', 'error creating role entry'] as const).includes(ret) && + options.addToModlog + ) client.emit( 'bushPunishRole', this, @@ -181,7 +196,10 @@ export class BushGuildMember extends GuildMember { return 'success'; })(); - if (!['error removing role', 'error creating modlog entry', 'error removing role entry'].includes(ret) && options.addToModlog) + if ( + !(['error removing role', 'error creating modlog entry', 'error removing role entry'] as const).includes(ret) && + options.addToModlog + ) client.emit( 'bushPunishRoleRemove', this, @@ -280,7 +298,7 @@ export class BushGuildMember extends GuildMember { return 'success'; })(); - if (!['error giving mute role', 'error creating modlog entry', 'error creating mute entry'].includes(ret)) + if (!(['error giving mute role', 'error creating modlog entry', 'error creating mute entry'] as const).includes(ret)) client.emit( 'bushMute', this, @@ -302,7 +320,7 @@ export class BushGuildMember extends GuildMember { * @emits {@link BushClientEvents.bushUnmute} */ public async unmute(options: BushPunishmentOptions): Promise<UnmuteResponse> { - //checks + // checks if (!this.guild.me!.permissions.has('MANAGE_ROLES')) return 'missing permissions'; const muteRoleID = await this.guild.getSetting('muteRole'); if (!muteRoleID) return 'no mute role'; @@ -315,7 +333,7 @@ export class BushGuildMember extends GuildMember { const moderator = (await util.resolveNonCachedUser(options.moderator ?? this.guild.me))!; const ret = await (async () => { - //remove role + // remove role const muteSuccess = await this.roles .remove(muteRole, `[Unmute] ${moderator.tag} | ${options.reason ?? 'No reason provided.'}`) .catch(async (e) => { @@ -324,7 +342,7 @@ export class BushGuildMember extends GuildMember { }); if (!muteSuccess) return 'error removing mute role'; - //remove modlog entry + // add modlog entry const { log: modlog } = await Moderation.createModLogEntry({ type: ModLogType.UNMUTE, user: this, @@ -346,7 +364,7 @@ export class BushGuildMember extends GuildMember { if (!removePunishmentEntrySuccess) return 'error removing mute entry'; - //dm user + // dm user const dmSuccess = await this.punishDM('unmuted', options.reason, undefined, false); dmSuccessEvent = dmSuccess; @@ -355,7 +373,7 @@ export class BushGuildMember extends GuildMember { return 'success'; })(); - if (!['error removing mute role', 'error creating modlog entry', 'error removing mute entry'].includes(ret)) + if (!(['error removing mute role', 'error creating modlog entry', 'error removing mute entry'] as const).includes(ret)) client.emit( 'bushUnmute', this, @@ -405,7 +423,7 @@ export class BushGuildMember extends GuildMember { if (!dmSuccess) return 'failed to dm'; return 'success'; })(); - if (!['error kicking', 'error creating modlog entry'].includes(ret)) + if (!(['error kicking', 'error creating modlog entry'] as const).includes(ret)) client.emit( 'bushKick', this, @@ -470,7 +488,7 @@ export class BushGuildMember extends GuildMember { if (!dmSuccess) return 'failed to dm'; return 'success'; })(); - if (!['error banning', 'error creating modlog entry', 'error creating ban entry'].includes(ret)) + if (!(['error banning', 'error creating modlog entry', 'error creating ban entry'] as const).includes(ret)) client.emit( 'bushBan', this, @@ -486,6 +504,161 @@ export class BushGuildMember extends GuildMember { } /** + * Prevents a user from speaking in a channel. + * @param options Options for blocking the user. + */ + public async block(options: BlockOptions): Promise<BlockResponse> { + const _channel = this.guild.channels.resolve(options.channel); + if (!_channel || (!_channel.isText() && !_channel.isThread())) return 'invalid channel'; + const channel = _channel as BushGuildTextBasedChannel; + + // checks + if (!channel.permissionsFor(this.guild.me!)!.has('MANAGE_CHANNELS')) 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 () => { + // change channel permissions + const channelToUse = channel.isThread() ? channel.parent! : channel; + const perm = channel.isThread() ? { SEND_MESSAGES_IN_THREADS: false } : { SEND_MESSAGES: false }; + const blockSuccess = await channelToUse.permissionOverwrites + .edit(this, perm, { reason: `[Block] ${moderator.tag} | ${options.reason ?? 'No reason provided.'}` }) + .catch(() => false); + if (!blockSuccess) return 'error blocking'; + + // add modlog entry + const { log: modlog } = await Moderation.createModLogEntry({ + type: options.duration ? ModLogType.TEMP_CHANNEL_BLOCK : ModLogType.PERM_CHANNEL_BLOCK, + user: this, + moderator: moderator.id, + reason: options.reason, + guild: this.guild, + evidence: options.evidence + }); + if (!modlog) return 'error creating modlog entry'; + caseID = modlog.id; + + // add punishment entry so they can be unblocked later + const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ + type: 'block', + user: this, + guild: this.guild, + duration: options.duration, + modlog: modlog.id, + extraInfo: channel.id + }); + if (!punishmentEntrySuccess) return 'error creating block entry'; + + // dm user + const dmSuccess = await this.send({ + content: `You have been blocked from <#${channel.id}> in **${this.guild.name}** ${ + options.duration !== null && options.duration !== undefined + ? options.duration + ? `for ${util.humanizeDuration(options.duration)} ` + : 'permanently ' + : '' + }for **${options.reason?.trim() ?? 'No reason provided'}**.` + }).catch(() => false); + dmSuccessEvent = !!dmSuccess; + + if (!dmSuccess) return 'failed to dm'; + + return 'success'; + })(); + + if (!(['error creating modlog entry', 'error creating block entry', 'error blocking'] as const).includes(ret)) + client.emit( + 'bushBlock', + this, + moderator, + this.guild, + options.reason ?? undefined, + caseID!, + options.duration ?? 0, + dmSuccessEvent!, + channel, + options.evidence + ); + return ret; + } + + /** + * Allows a user to speak in a channel. + * @param options Options for unblocking the user. + */ + public async unblock(options: UnblockOptions): Promise<UnblockResponse> { + const _channel = this.guild.channels.resolve(options.channel); + if (!_channel || (!_channel.isText() && !_channel.isThread())) return 'invalid channel'; + const channel = _channel as BushGuildTextBasedChannel; + + // checks + if (!channel.permissionsFor(this.guild.me!)!.has('MANAGE_CHANNELS')) 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 () => { + // change channel permissions + const channelToUse = channel.isThread() ? channel.parent! : channel; + const perm = channel.isThread() ? { SEND_MESSAGES_IN_THREADS: null } : { SEND_MESSAGES: null }; + const blockSuccess = await channelToUse.permissionOverwrites + .edit(this, perm, { reason: `[Unblock] ${moderator.tag} | ${options.reason ?? 'No reason provided.'}` }) + .catch(() => false); + if (!blockSuccess) return 'error unblocking'; + + // add modlog entry + const { log: modlog } = await Moderation.createModLogEntry({ + type: ModLogType.CHANNEL_UNBLOCK, + user: this, + moderator: moderator.id, + reason: options.reason, + guild: this.guild, + evidence: options.evidence + }); + if (!modlog) return 'error creating modlog entry'; + caseID = modlog.id; + + // remove punishment entry + const punishmentEntrySuccess = await Moderation.removePunishmentEntry({ + type: 'block', + user: this, + guild: this.guild, + extraInfo: channel.id + }); + if (!punishmentEntrySuccess) return 'error removing block entry'; + + // dm user + const dmSuccess = await this.send({ + content: `You have been unblocked from <#${channel.id}> in **${this.guild.name}** for **${ + options.reason?.trim() ?? 'No reason provided' + }**.` + }).catch(() => false); + dmSuccessEvent = !!dmSuccess; + + if (!dmSuccess) return 'failed to dm'; + + return 'success'; + })(); + + if (!(['error creating modlog entry', 'error removing block entry', 'error unblocking'] as const).includes(ret)) + client.emit( + 'bushUnblock', + this, + moderator, + this.guild, + options.reason ?? undefined, + caseID!, + dmSuccessEvent!, + channel, + options.evidence + ); + return ret; + } + + /** * Whether or not the user is an owner of the bot. */ public isOwner(): boolean { @@ -503,7 +676,7 @@ export class BushGuildMember extends GuildMember { /** * Options for punishing a user. */ -interface BushPunishmentOptions { +export interface BushPunishmentOptions { /** * The reason for the punishment. */ @@ -523,7 +696,7 @@ interface BushPunishmentOptions { /** * Punishment options for punishments that can be temporary. */ -interface BushTimedPunishmentOptions extends BushPunishmentOptions { +export interface BushTimedPunishmentOptions extends BushPunishmentOptions { /** * The duration of the punishment. */ @@ -533,7 +706,7 @@ interface BushTimedPunishmentOptions extends BushPunishmentOptions { /** * Options for a role add punishment. */ -interface AddRoleOptions extends BushTimedPunishmentOptions { +export interface AddRoleOptions extends BushTimedPunishmentOptions { /** * The role to add to the user. */ @@ -548,7 +721,7 @@ interface AddRoleOptions extends BushTimedPunishmentOptions { /** * Options for a role remove punishment. */ -interface RemoveRoleOptions extends BushTimedPunishmentOptions { +export interface RemoveRoleOptions extends BushTimedPunishmentOptions { /** * The role to remove from the user. */ @@ -563,24 +736,44 @@ interface RemoveRoleOptions extends BushTimedPunishmentOptions { /** * Options for banning a user. */ -interface BushBanOptions extends BushTimedPunishmentOptions { +export interface BushBanOptions extends BushTimedPunishmentOptions { /** * The number of days to delete the user's messages for. */ deleteDays?: number; } -type PunishmentResponse = 'success' | 'error creating modlog entry' | 'failed to dm'; +/** + * Options for blocking a user from a channel. + */ +export interface BlockOptions extends BushTimedPunishmentOptions { + /** + * The channel to block the user from. + */ + channel: BushGuildTextChannelResolvable | BushThreadChannelResolvable; +} + +/** + * Options for unblocking a user from a channel. + */ +export interface UnblockOptions extends BushPunishmentOptions { + /** + * The channel to unblock the user from. + */ + channel: BushGuildTextChannelResolvable | BushThreadChannelResolvable; +} + +export type PunishmentResponse = 'success' | 'error creating modlog entry' | 'failed to dm'; /** * Response returned when warning a user. */ -type WarnResponse = PunishmentResponse; +export type WarnResponse = PunishmentResponse; /** * Response returned when adding a role to a user. */ -type AddRoleResponse = +export type AddRoleResponse = | PunishmentResponse | 'user hierarchy' | 'role managed' @@ -591,7 +784,7 @@ type AddRoleResponse = /** * Response returned when removing a role from a user. */ -type RemoveRoleResponse = +export type RemoveRoleResponse = | PunishmentResponse | 'user hierarchy' | 'role managed' @@ -602,7 +795,7 @@ type RemoveRoleResponse = /** * Response returned when muting a user. */ -type MuteResponse = +export type MuteResponse = | PunishmentResponse | 'missing permissions' | 'no mute role' @@ -614,7 +807,7 @@ type MuteResponse = /** * Response returned when unmuting a user. */ -type UnmuteResponse = +export type UnmuteResponse = | PunishmentResponse | 'missing permissions' | 'no mute role' @@ -626,17 +819,37 @@ type UnmuteResponse = /** * Response returned when kicking a user. */ -type KickResponse = PunishmentResponse | 'missing permissions' | 'error kicking'; +export type KickResponse = PunishmentResponse | 'missing permissions' | 'error kicking'; /** * Response returned when banning a user. */ -type BanResponse = PunishmentResponse | 'missing permissions' | 'error creating ban entry' | 'error banning'; +export type BanResponse = PunishmentResponse | 'missing permissions' | 'error creating ban entry' | 'error banning'; + +/** + * Response returned when blocking a user. + */ +export type BlockResponse = + | PunishmentResponse + | 'invalid channel' + | 'error creating block entry' + | 'missing permissions' + | 'error blocking'; + +/** + * Response returned when unblocking a user. + */ +export type UnblockResponse = + | PunishmentResponse + | 'invalid channel' + | 'error removing block entry' + | 'missing permissions' + | 'error unblocking'; export type PartialBushGuildMember = Partialize< BushGuildMember, 'joinedAt' | 'joinedTimestamp', - 'warn' | 'addRole' | 'removeRole' | 'mute' | 'unmute' | 'bushKick' | 'bushBan' | 'isOwner' | 'isSuperUser' + 'warn' | 'addRole' | 'removeRole' | 'mute' | 'unmute' | 'bushKick' | 'bushBan' | 'isOwner' | 'isSuperUser' | 'block' >; /** |