diff options
author | IRONM00N <64110067+IRONM00N@users.noreply.github.com> | 2022-08-18 22:42:12 -0400 |
---|---|---|
committer | IRONM00N <64110067+IRONM00N@users.noreply.github.com> | 2022-08-18 22:42:12 -0400 |
commit | 2356d2c44736fb83021dacb551625852111c8ce6 (patch) | |
tree | 10408d22fdd7a358d2f5c5917c3b59e55aa4c19d /lib/extensions/discord.js | |
parent | 8aed6f93f7740c592cbc0e2f9fd3269c05286077 (diff) | |
download | tanzanite-2356d2c44736fb83021dacb551625852111c8ce6.tar.gz tanzanite-2356d2c44736fb83021dacb551625852111c8ce6.tar.bz2 tanzanite-2356d2c44736fb83021dacb551625852111c8ce6.zip |
restructure, experimental presence and member automod, fixed bugs probably made some more bugs
Diffstat (limited to 'lib/extensions/discord.js')
-rw-r--r-- | lib/extensions/discord.js/BushClientEvents.ts | 200 | ||||
-rw-r--r-- | lib/extensions/discord.js/ExtendedGuild.ts | 919 | ||||
-rw-r--r-- | lib/extensions/discord.js/ExtendedGuildMember.ts | 1255 | ||||
-rw-r--r-- | lib/extensions/discord.js/ExtendedMessage.ts | 12 | ||||
-rw-r--r-- | lib/extensions/discord.js/ExtendedUser.ts | 35 |
5 files changed, 2421 insertions, 0 deletions
diff --git a/lib/extensions/discord.js/BushClientEvents.ts b/lib/extensions/discord.js/BushClientEvents.ts new file mode 100644 index 0000000..22bae65 --- /dev/null +++ b/lib/extensions/discord.js/BushClientEvents.ts @@ -0,0 +1,200 @@ +import type { + BanResponse, + CommandMessage, + Guild as GuildDB, + GuildSettings +} from '#lib'; +import type { AkairoClientEvents } from 'discord-akairo'; +import type { + ButtonInteraction, + Collection, + Guild, + GuildMember, + GuildTextBasedChannel, + Message, + ModalSubmitInteraction, + Role, + SelectMenuInteraction, + Snowflake, + User +} from 'discord.js'; + +export interface BushClientEvents extends AkairoClientEvents { + bushBan: [ + victim: GuildMember | User, + moderator: User, + guild: Guild, + reason: string | undefined, + caseID: string, + duration: number, + dmSuccess?: boolean, + evidence?: string + ]; + bushBlock: [ + victim: GuildMember, + moderator: User, + guild: Guild, + reason: string | undefined, + caseID: string, + duration: number, + dmSuccess: boolean, + channel: GuildTextBasedChannel, + evidence?: string + ]; + bushKick: [ + victim: GuildMember, + moderator: User, + guild: Guild, + reason: string | undefined, + caseID: string, + dmSuccess: boolean, + evidence?: string + ]; + bushMute: [ + victim: GuildMember, + moderator: User, + guild: Guild, + reason: string | undefined, + caseID: string, + duration: number, + dmSuccess: boolean, + evidence?: string + ]; + bushPunishRole: [ + victim: GuildMember, + moderator: User, + guild: Guild, + reason: string | undefined, + caseID: string, + duration: number, + role: Role, + evidence?: string + ]; + bushPunishRoleRemove: [ + victim: GuildMember, + moderator: User, + guild: Guild, + reason: string | undefined, + caseID: string, + role: Role, + evidence?: string + ]; + bushPurge: [ + moderator: User, + guild: Guild, + channel: GuildTextBasedChannel, + messages: Collection<Snowflake, Message> + ]; + bushRemoveTimeout: [ + victim: GuildMember, + moderator: User, + guild: Guild, + reason: string | undefined, + caseID: string, + dmSuccess: boolean, + evidence?: string + ]; + bushTimeout: [ + victim: GuildMember, + moderator: User, + guild: Guild, + reason: string | undefined, + caseID: string, + duration: number, + dmSuccess: boolean, + evidence?: string + ]; + bushUnban: [ + victim: User, + moderator: User, + guild: Guild, + reason: string | undefined, + caseID: string, + dmSuccess: boolean, + evidence?: string + ]; + bushUnblock: [ + victim: GuildMember | User, + moderator: User, + guild: Guild, + reason: string | undefined, + caseID: string, + dmSuccess: boolean, + channel: GuildTextBasedChannel, + evidence?: string + ]; + bushUnmute: [ + victim: GuildMember, + moderator: User, + guild: Guild, + reason: string | undefined, + caseID: string, + dmSuccess: boolean, + evidence?: string + ]; + bushUpdateModlog: [ + moderator: GuildMember, + modlogID: string, + key: 'evidence' | 'hidden', + oldModlog: string | boolean, + newModlog: string | boolean + ]; + bushUpdateSettings: [ + setting: Setting, + guild: Guild, + oldValue: GuildDB[Setting], + newValue: GuildDB[Setting], + moderator?: GuildMember + ]; + bushWarn: [ + victim: GuildMember, + moderator: User, + guild: Guild, + reason: string | undefined, + caseID: string, + dmSuccess: boolean, + evidence?: string + ]; + bushLevelUpdate: [ + member: GuildMember, + oldLevel: number, + newLevel: number, + currentXp: number, + message: CommandMessage + ]; + bushLockdown: [ + moderator: GuildMember, + reason: string | undefined, + channelsSuccessMap: Collection<Snowflake, boolean>, + all?: boolean + ]; + bushUnlockdown: [ + moderator: GuildMember, + reason: string | undefined, + channelsSuccessMap: Collection<Snowflake, boolean>, + all?: boolean + ]; + massBan: [ + moderator: GuildMember, + guild: Guild, + reason: string | undefined, + results: Collection<Snowflake, BanResponse> + ]; + massEvidence: [ + moderator: GuildMember, + guild: Guild, + evidence: string, + lines: string[] + ]; + /* components */ + button: [button: ButtonInteraction]; + selectMenu: [selectMenu: SelectMenuInteraction]; + modal: [modal: ModalSubmitInteraction]; +} + +type Setting = + | GuildSettings + | 'enabledFeatures' + | 'blacklistedChannels' + | 'blacklistedUsers' + | 'disabledCommands'; diff --git a/lib/extensions/discord.js/ExtendedGuild.ts b/lib/extensions/discord.js/ExtendedGuild.ts new file mode 100644 index 0000000..63ee2fd --- /dev/null +++ b/lib/extensions/discord.js/ExtendedGuild.ts @@ -0,0 +1,919 @@ +import { + AllowedMentions, + banResponse, + colors, + dmResponse, + emojis, + permissionsResponse, + punishmentEntryRemove, + type BanResponse, + type GuildFeatures, + type GuildLogType, + type GuildModel +} from '#lib'; +import assert from 'assert/strict'; +import { + AttachmentBuilder, + AttachmentPayload, + Collection, + Guild, + JSONEncodable, + Message, + MessageType, + PermissionFlagsBits, + SnowflakeUtil, + ThreadChannel, + type APIMessage, + type GuildMember, + type GuildMemberResolvable, + type GuildTextBasedChannel, + type MessageOptions, + type MessagePayload, + type NewsChannel, + type Snowflake, + type TextChannel, + type User, + type UserResolvable, + type VoiceChannel, + type Webhook, + type WebhookMessageOptions +} from 'discord.js'; +import _ from 'lodash'; +import * as Moderation from '../../common/Moderation.js'; +import { Guild as GuildDB } from '../../models/instance/Guild.js'; +import { ModLogType } from '../../models/instance/ModLog.js'; +import { addOrRemoveFromArray } from '../../utils/BushUtils.js'; + +declare module 'discord.js' { + export interface Guild { + /** + * Checks if the guild has a certain custom feature. + * @param feature The feature to check for + */ + hasFeature(feature: GuildFeatures): Promise<boolean>; + /** + * Adds a custom feature to the guild. + * @param feature The feature to add + * @param moderator The moderator responsible for adding a feature + */ + addFeature(feature: GuildFeatures, moderator?: GuildMember): Promise<GuildDB['enabledFeatures']>; + /** + * Removes a custom feature from the guild. + * @param feature The feature to remove + * @param moderator The moderator responsible for removing a feature + */ + removeFeature(feature: GuildFeatures, moderator?: GuildMember): Promise<GuildDB['enabledFeatures']>; + /** + * Makes a custom feature the opposite of what it was before + * @param feature The feature to toggle + * @param moderator The moderator responsible for toggling a feature + */ + toggleFeature(feature: GuildFeatures, moderator?: GuildMember): Promise<GuildDB['enabledFeatures']>; + /** + * Fetches a custom setting for the guild + * @param setting The setting to get + */ + getSetting<K extends keyof GuildModel>(setting: K): Promise<GuildModel[K]>; + /** + * Sets a custom setting for the guild + * @param setting The setting to change + * @param value The value to change the setting to + * @param moderator The moderator to responsible for changing the setting + */ + setSetting<K extends Exclude<keyof GuildModel, 'id'>>( + setting: K, + value: GuildModel[K], + moderator?: GuildMember + ): Promise<GuildModel>; + /** + * Get a the log channel configured for a certain log type. + * @param logType The type of log channel to get. + * @returns Either the log channel or undefined if not configured. + */ + getLogChannel(logType: GuildLogType): Promise<TextChannel | undefined>; + /** + * Sends a message to the guild's specified logging channel + * @param logType The corresponding channel that the message will be sent to + * @param message The parameters for {@link BushTextChannel.send} + */ + sendLogChannel(logType: GuildLogType, message: string | MessagePayload | MessageOptions): Promise<Message | null | undefined>; + /** + * Sends a formatted error message in a guild's error log channel + * @param title The title of the error embed + * @param message The description of the error embed + */ + error(title: string, message: string): Promise<void>; + /** + * Bans a user, dms them, creates a mod log entry, and creates a punishment entry. + * @param options Options for banning the user. + * @returns A string status message of the ban. + */ + bushBan(options: GuildBushBanOptions): Promise<BanResponse>; + /** + * {@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 + */ + massBanOne(options: GuildMassBanOneOptions): Promise<BanResponse>; + /** + * 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. + */ + bushUnban(options: GuildBushUnbanOptions): Promise<UnbanResponse>; + /** + * Denies send permissions in specified channels + * @param options The options for locking down the guild + */ + lockdown(options: LockdownOptions): Promise<LockdownResponse>; + quote(rawQuote: APIMessage, channel: GuildTextBasedChannel): Promise<Message | null>; + } +} + +/** + * 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 ExtendedGuild.available}.</info> + */ +export class ExtendedGuild extends Guild { + /** + * Checks if the guild has a certain custom feature. + * @param feature The feature to check for + */ + public override async hasFeature(feature: GuildFeatures): Promise<boolean> { + const features = await this.getSetting('enabledFeatures'); + return features.includes(feature); + } + + /** + * Adds a custom feature to the guild. + * @param feature The feature to add + * @param moderator The moderator responsible for adding a feature + */ + public override async addFeature(feature: GuildFeatures, moderator?: GuildMember): Promise<GuildModel['enabledFeatures']> { + const features = await this.getSetting('enabledFeatures'); + const newFeatures = addOrRemoveFromArray('add', features, feature); + return (await this.setSetting('enabledFeatures', newFeatures, moderator)).enabledFeatures; + } + + /** + * Removes a custom feature from the guild. + * @param feature The feature to remove + * @param moderator The moderator responsible for removing a feature + */ + public override async removeFeature(feature: GuildFeatures, moderator?: GuildMember): Promise<GuildModel['enabledFeatures']> { + const features = await this.getSetting('enabledFeatures'); + const newFeatures = addOrRemoveFromArray('remove', features, feature); + return (await this.setSetting('enabledFeatures', newFeatures, moderator)).enabledFeatures; + } + + /** + * Makes a custom feature the opposite of what it was before + * @param feature The feature to toggle + * @param moderator The moderator responsible for toggling a feature + */ + public override async toggleFeature(feature: GuildFeatures, moderator?: GuildMember): Promise<GuildModel['enabledFeatures']> { + return (await this.hasFeature(feature)) + ? await this.removeFeature(feature, moderator) + : await this.addFeature(feature, moderator); + } + + /** + * Fetches a custom setting for the guild + * @param setting The setting to get + */ + public override async getSetting<K extends keyof GuildModel>(setting: K): Promise<GuildModel[K]> { + return ( + this.client.cache.guilds.get(this.id)?.[setting] ?? + ((await GuildDB.findByPk(this.id)) ?? GuildDB.build({ id: this.id }))[setting] + ); + } + + /** + * Sets a custom setting for the guild + * @param setting The setting to change + * @param value The value to change the setting to + * @param moderator The moderator to responsible for changing the setting + */ + public override async setSetting<K extends Exclude<keyof GuildModel, 'id'>>( + setting: K, + value: GuildDB[K], + moderator?: GuildMember + ): Promise<GuildDB> { + const row = (await GuildDB.findByPk(this.id)) ?? GuildDB.build({ id: this.id }); + const oldValue = row[setting] as GuildDB[K]; + row[setting] = value; + this.client.cache.guilds.set(this.id, row.toJSON() as GuildDB); + this.client.emit('bushUpdateSettings', setting, this, oldValue, row[setting], moderator); + return await row.save(); + } + + /** + * Get a the log channel configured for a certain log type. + * @param logType The type of log channel to get. + * @returns Either the log channel or undefined if not configured. + */ + public override async getLogChannel(logType: GuildLogType): Promise<TextChannel | undefined> { + const channelId = (await this.getSetting('logChannels'))[logType]; + if (!channelId) return undefined; + return ( + (this.channels.cache.get(channelId) as TextChannel | undefined) ?? + ((await this.channels.fetch(channelId)) as TextChannel | null) ?? + undefined + ); + } + + /** + * Sends a message to the guild's specified logging channel + * @param logType The corresponding channel that the message will be sent to + * @param message The parameters for {@link BushTextChannel.send} + */ + public override async sendLogChannel( + logType: GuildLogType, + message: string | MessagePayload | MessageOptions + ): Promise<Message | null | undefined> { + const logChannel = await this.getLogChannel(logType); + if (!logChannel || !logChannel.isTextBased()) { + void this.client.console.warn('sendLogChannel', `No log channel found for <<${logType}<< in <<${this.name}>>.`); + return; + } + if ( + !logChannel + .permissionsFor(this.members.me!.id) + ?.has([PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.EmbedLinks]) + ) + return; + + return await logChannel.send(message).catch(() => null); + } + + /** + * Sends a formatted error message in a guild's error log channel + * @param title The title of the error embed + * @param message The description of the error embed + */ + public override async error(title: string, message: string): Promise<void> { + void this.client.console.info(_.camelCase(title), message.replace(/\*\*(.*?)\*\*/g, '<<$1>>')); + void this.sendLogChannel('error', { embeds: [{ title: title, description: message, color: colors.error }] }); + } + + /** + * Bans a user, dms them, creates a mod log entry, and creates a punishment entry. + * @param options Options for banning the user. + * @returns A string status message of the ban. + */ + public override async bushBan(options: GuildBushBanOptions): Promise<BanResponse> { + // checks + if (!this.members.me!.permissions.has(PermissionFlagsBits.BanMembers)) return banResponse.MISSING_PERMISSIONS; + + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; + const user = await this.client.utils.resolveNonCachedUser(options.user); + const moderator = this.client.users.resolve(options.moderator ?? this.client.user!); + if (!user || !moderator) return banResponse.CANNOT_RESOLVE_USER; + + if ((await this.bans.fetch()).has(user.id)) return banResponse.ALREADY_BANNED; + + const ret = await (async () => { + // add modlog entry + const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, + type: options.duration ? ModLogType.TEMP_BAN : ModLogType.PERM_BAN, + user: user, + moderator: moderator.id, + reason: options.reason, + duration: options.duration, + guild: this, + evidence: options.evidence + }); + if (!modlog) return banResponse.MODLOG_ERROR; + caseID = modlog.id; + + // dm user + dmSuccessEvent = await Moderation.punishDM({ + client: this.client, + modlog: modlog.id, + guild: this, + user: user, + punishment: 'banned', + duration: options.duration ?? 0, + reason: options.reason ?? undefined, + sendFooter: true + }); + + // ban + const banSuccess = await this.bans + .create(user?.id ?? options.user, { + reason: `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`, + 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({ + client: this.client, + type: 'ban', + user: user, + guild: this, + duration: options.duration, + modlog: modlog.id + }); + if (!punishmentEntrySuccess) return banResponse.PUNISHMENT_ENTRY_ADD_ERROR; + + if (!dmSuccessEvent) return banResponse.DM_ERROR; + return banResponse.SUCCESS; + })(); + + if (!([banResponse.ACTION_ERROR, banResponse.MODLOG_ERROR, banResponse.PUNISHMENT_ENTRY_ADD_ERROR] as const).includes(ret)) + this.client.emit( + 'bushBan', + user, + moderator, + this, + options.reason ?? undefined, + caseID!, + options.duration ?? 0, + dmSuccessEvent, + options.evidence + ); + return ret; + } + + /** + * {@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 override 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({ + client: this.client, + 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({ + client: this.client, + 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({ + client: this.client, + 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. + */ + public override async bushUnban(options: GuildBushUnbanOptions): Promise<UnbanResponse> { + // checks + if (!this.members.me!.permissions.has(PermissionFlagsBits.BanMembers)) return unbanResponse.MISSING_PERMISSIONS; + + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; + const user = await this.client.utils.resolveNonCachedUser(options.user); + const moderator = this.client.users.resolve(options.moderator ?? this.client.user!); + if (!user || !moderator) return unbanResponse.CANNOT_RESOLVE_USER; + + 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 unbanResponse.NOT_BANNED; + if (!unbanSuccess) return unbanResponse.ACTION_ERROR; + + // add modlog entry + const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, + type: ModLogType.UNBAN, + user: user.id, + moderator: moderator.id, + reason: options.reason, + guild: this, + evidence: options.evidence + }); + if (!modlog) return unbanResponse.MODLOG_ERROR; + caseID = modlog.id; + + // remove punishment entry + const removePunishmentEntrySuccess = await Moderation.removePunishmentEntry({ + client: this.client, + type: 'ban', + user: user.id, + guild: this + }); + if (!removePunishmentEntrySuccess) return unbanResponse.PUNISHMENT_ENTRY_REMOVE_ERROR; + + // dm user + dmSuccessEvent = await Moderation.punishDM({ + client: this.client, + guild: this, + user: user, + punishment: 'unbanned', + reason: options.reason ?? undefined, + sendFooter: false + }); + + if (!dmSuccessEvent) return unbanResponse.DM_ERROR; + return unbanResponse.SUCCESS; + })(); + if ( + !([unbanResponse.ACTION_ERROR, unbanResponse.MODLOG_ERROR, unbanResponse.PUNISHMENT_ENTRY_REMOVE_ERROR] as const).includes( + ret + ) + ) + this.client.emit( + 'bushUnban', + user, + moderator, + this, + options.reason ?? undefined, + caseID!, + dmSuccessEvent!, + options.evidence + ); + return ret; + } + + /** + * Denies send permissions in specified channels + * @param options The options for locking down the guild + */ + public override 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>(); + const success = new Collection<Snowflake, boolean>(); + const ret = await (async (): Promise<LockdownResponse> => { + for (const _channel of mappedChannels) { + const channel = _channel!; + if (!channel.isTextBased()) { + errors.set(channel.id, new Error('wrong channel type')); + success.set(channel.id, false); + continue; + } + if (!channel.permissionsFor(this.members.me!.id)?.has([PermissionFlagsBits.ManageChannels])) { + errors.set(channel.id, new Error('client no permission')); + success.set(channel.id, false); + continue; + } else if (!channel.permissionsFor(moderator)?.has([PermissionFlagsBits.ManageChannels])) { + errors.set(channel.id, new Error('moderator no permission')); + success.set(channel.id, false); + continue; + } + + const reason = `[${options.unlock ? 'Unlockdown' : 'Lockdown'}] ${moderator.user.tag} | ${ + options.reason ?? 'No reason provided' + }`; + + const permissionOverwrites = channel.isThread() ? channel.parent!.permissionOverwrites : channel.permissionOverwrites; + const perms = { + SendMessagesInThreads: options.unlock ? null : false, + SendMessages: options.unlock ? null : false + }; + const permsForMe = { + [channel.isThread() ? 'SendMessagesInThreads' : 'SendMessages']: options.unlock ? null : true + }; // so I can send messages in the channel + + const changePermSuccess = await permissionOverwrites.edit(this.id, perms, { reason }).catch((e) => e); + if (changePermSuccess instanceof Error) { + errors.set(channel.id, changePermSuccess); + success.set(channel.id, false); + } else { + success.set(channel.id, true); + await permissionOverwrites.edit(this.members.me!, permsForMe, { reason }); + await channel.send({ + embeds: [ + { + author: { name: moderator.user.tag, icon_url: moderator.displayAvatarURL() }, + title: `This channel has been ${options.unlock ? 'un' : ''}locked`, + description: options.reason ?? 'No reason provided', + color: options.unlock ? colors.Green : colors.Red, + timestamp: new Date().toISOString() + } + ] + }); + } + } + + if (errors.size) return errors; + else return `success: ${success.filter((c) => c === true).size}`; + })(); + + this.client.emit(options.unlock ? 'bushUnlockdown' : 'bushLockdown', moderator, options.reason, success, options.all); + return ret; + } + + public override async quote(rawQuote: APIMessage, channel: GuildTextBasedChannel): Promise<Message | null> { + if (!channel.isTextBased() || channel.isDMBased() || channel.guildId !== this.id || !this.members.me) return null; + if (!channel.permissionsFor(this.members.me).has('ManageWebhooks')) return null; + + const quote = new Message(this.client, rawQuote); + + const target = channel instanceof ThreadChannel ? channel.parent : channel; + if (!target) return null; + + const webhooks: Collection<string, Webhook> = await target.fetchWebhooks().catch((e) => e); + if (!(webhooks instanceof Collection)) return null; + + // find a webhook that we can use + let webhook = webhooks.find((w) => !!w.token) ?? null; + if (!webhook) + webhook = await target + .createWebhook({ + name: `${this.client.user!.username} Quotes #${target.name}`, + avatar: this.client.user!.displayAvatarURL({ size: 2048 }), + reason: 'Creating a webhook for quoting' + }) + .catch(() => null); + + if (!webhook) return null; + + const sendOptions: Omit<WebhookMessageOptions, 'flags'> = {}; + + const displayName = quote.member?.displayName ?? quote.author.username; + + switch (quote.type) { + case MessageType.Default: + case MessageType.Reply: + case MessageType.ChatInputCommand: + case MessageType.ContextMenuCommand: + case MessageType.ThreadStarterMessage: + sendOptions.content = quote.content || undefined; + sendOptions.threadId = channel instanceof ThreadChannel ? channel.id : undefined; + sendOptions.embeds = quote.embeds.length ? quote.embeds : undefined; + //@ts-expect-error: jank + sendOptions.attachments = quote.attachments.size + ? [...quote.attachments.values()].map((a) => AttachmentBuilder.from(a as JSONEncodable<AttachmentPayload>)) + : undefined; + + if (quote.stickers.size && !(quote.content || quote.embeds.length || quote.attachments.size)) + sendOptions.content = '[[This message has a sticker but not content]]'; + + break; + case MessageType.RecipientAdd: { + const recipient = rawQuote.mentions[0]; + if (!recipient) { + sendOptions.content = `${emojis.error} Cannot resolve recipient.`; + break; + } + + if (quote.channel.isThread()) { + const recipientDisplay = quote.guild?.members.cache.get(recipient.id)?.displayName ?? recipient.username; + sendOptions.content = `${emojis.join} ${displayName} added ${recipientDisplay} to the thread.`; + } else { + // this should never happen + sendOptions.content = `${emojis.join} ${displayName} added ${recipient.username} to the group.`; + } + + break; + } + case MessageType.RecipientRemove: { + const recipient = rawQuote.mentions[0]; + if (!recipient) { + sendOptions.content = `${emojis.error} Cannot resolve recipient.`; + break; + } + + if (quote.channel.isThread()) { + const recipientDisplay = quote.guild?.members.cache.get(recipient.id)?.displayName ?? recipient.username; + sendOptions.content = `${emojis.leave} ${displayName} removed ${recipientDisplay} from the thread.`; + } else { + // this should never happen + sendOptions.content = `${emojis.leave} ${displayName} removed ${recipient.username} from the group.`; + } + + break; + } + + case MessageType.ChannelNameChange: + sendOptions.content = `<:pencil:957988608994861118> ${displayName} changed the channel name: **${quote.content}**`; + + break; + + case MessageType.ChannelPinnedMessage: + throw new Error('Not implemented yet: MessageType.ChannelPinnedMessage case'); + case MessageType.UserJoin: { + const messages = [ + '{username} joined the party.', + '{username} is here.', + 'Welcome, {username}. We hope you brought pizza.', + 'A wild {username} appeared.', + '{username} just landed.', + '{username} just slid into the server.', + '{username} just showed up!', + 'Welcome {username}. Say hi!', + '{username} hopped into the server.', + 'Everyone welcome {username}!', + "Glad you're here, {username}.", + 'Good to see you, {username}.', + 'Yay you made it, {username}!' + ]; + + const timestamp = SnowflakeUtil.timestampFrom(quote.id); + + // this is the same way that the discord client decides what message to use. + const message = messages[timestamp % messages.length].replace(/{username}/g, displayName); + + sendOptions.content = `${emojis.join} ${message}`; + break; + } + case MessageType.GuildBoost: + sendOptions.content = `<:NitroBoost:585558042309820447> ${displayName} just boosted the server${ + quote.content ? ` **${quote.content}** times` : '' + }!`; + + break; + case MessageType.GuildBoostTier1: + case MessageType.GuildBoostTier2: + case MessageType.GuildBoostTier3: + sendOptions.content = `<:NitroBoost:585558042309820447> ${displayName} just boosted the server${ + quote.content ? ` **${quote.content}** times` : '' + }! ${quote.guild?.name} has achieved **Level ${quote.type - 8}!**`; + + break; + case MessageType.ChannelFollowAdd: + sendOptions.content = `${displayName} has added **${quote.content}** to this channel. Its most important updates will show up here.`; + + break; + case MessageType.GuildDiscoveryDisqualified: + sendOptions.content = + '<:SystemMessageCross:842172192418693173> This server has been removed from Server Discovery because it no longer passes all the requirements. Check Server Settings for more details.'; + + break; + case MessageType.GuildDiscoveryRequalified: + sendOptions.content = + '<:SystemMessageCheck:842172191801212949> This server is eligible for Server Discovery again and has been automatically relisted!'; + + break; + case MessageType.GuildDiscoveryGracePeriodInitialWarning: + sendOptions.content = + '<:SystemMessageWarn:842172192401915971> This server has failed Discovery activity requirements for 1 week. If this server fails for 4 weeks in a row, it will be automatically removed from Discovery.'; + + break; + case MessageType.GuildDiscoveryGracePeriodFinalWarning: + sendOptions.content = + '<:SystemMessageWarn:842172192401915971> This server has failed Discovery activity requirements for 3 weeks in a row. If this server fails for 1 more week, it will be removed from Discovery.'; + + break; + case MessageType.ThreadCreated: { + const threadId = rawQuote.message_reference?.channel_id; + + sendOptions.content = `<:thread:865033845753249813> ${displayName} started a thread: **[${quote.content}](https://discord.com/channels/${quote.guildId}/${threadId} + )**. See all threads.`; + + break; + } + case MessageType.GuildInviteReminder: + sendOptions.content = 'Wondering who to invite? Start by inviting anyone who can help you build the server!'; + + break; + // todo: use enum for this + case 24 as MessageType: { + const embed = quote.embeds[0]; + // eslint-disable-next-line deprecation/deprecation + assert.equal(embed.data.type, 'auto_moderation_message'); + const ruleName = embed.fields!.find((f) => f.name === 'rule_name')!.value; + const channelId = embed.fields!.find((f) => f.name === 'channel_id')!.value; + const keyword = embed.fields!.find((f) => f.name === 'keyword')!.value; + + sendOptions.username = `AutoMod (${quote.member?.displayName ?? quote.author.username})`; + sendOptions.content = `Automod has blocked a message in <#${channelId}>`; + sendOptions.embeds = [ + { + title: quote.member?.displayName ?? quote.author.username, + description: embed.description ?? 'There is no content???', + footer: { + text: `Keyword: ${keyword} • Rule: ${ruleName}` + }, + color: 0x36393f + } + ]; + + break; + } + case MessageType.ChannelIconChange: + case MessageType.Call: + default: + sendOptions.content = `${emojis.error} I cannot quote messages of type **${ + MessageType[quote.type] || quote.type + }** messages, please report this to my developers.`; + + break; + } + + sendOptions.allowedMentions = AllowedMentions.none(); + sendOptions.username ??= quote.member?.displayName ?? quote.author.username; + sendOptions.avatarURL = quote.member?.displayAvatarURL({ size: 2048 }) ?? quote.author.displayAvatarURL({ size: 2048 }); + + return await webhook.send(sendOptions); /* .catch((e: any) => e); */ + } +} + +/** + * Options for unbanning a user + */ +export interface GuildBushUnbanOptions { + /** + * The user to unban + */ + user: UserResolvable | User; + + /** + * The reason for unbanning the user + */ + reason?: string | null; + + /** + * The moderator who unbanned the user + */ + moderator?: UserResolvable; + + /** + * The evidence for the unban + */ + 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 + */ +export interface GuildBushBanOptions { + /** + * The user to ban + */ + user: UserResolvable; + + /** + * The reason to ban the user + */ + reason?: string | null; + + /** + * The moderator who banned the user + */ + moderator?: UserResolvable; + + /** + * The duration of the ban + */ + duration?: number; + + /** + * The number of days to delete the user's messages for + */ + deleteDays?: number; + + /** + * The evidence for the ban + */ + evidence?: string; +} + +type ValueOf<T> = T[keyof T]; + +export const unbanResponse = Object.freeze({ + ...dmResponse, + ...permissionsResponse, + ...punishmentEntryRemove, + NOT_BANNED: 'user not banned' +} as const); + +/** + * Response returned when unbanning a user + */ +export type UnbanResponse = ValueOf<typeof unbanResponse>; + +/** + * Options for locking down channel(s) + */ +export interface LockdownOptions { + /** + * The moderator responsible for the lockdown + */ + moderator: GuildMemberResolvable; + + /** + * Whether to lock down all (specified) channels + */ + all: boolean; + + /** + * Reason for the lockdown + */ + reason?: string; + + /** + * A specific channel to lockdown + */ + channel?: ThreadChannel | NewsChannel | TextChannel | VoiceChannel; + + /** + * Whether or not to unlock the channel(s) instead of locking them + */ + unlock?: boolean; +} + +/** + * Response returned when locking down a channel + */ +export 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/lib/extensions/discord.js/ExtendedGuildMember.ts b/lib/extensions/discord.js/ExtendedGuildMember.ts new file mode 100644 index 0000000..f8add83 --- /dev/null +++ b/lib/extensions/discord.js/ExtendedGuildMember.ts @@ -0,0 +1,1255 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { formatError, Moderation, ModLogType, Time, type BushClientEvents, type PunishmentTypeDM, type ValueOf } from '#lib'; +import { + ChannelType, + GuildMember, + PermissionFlagsBits, + type GuildChannelResolvable, + type GuildTextBasedChannel, + type Role +} from 'discord.js'; +/* eslint-enable @typescript-eslint/no-unused-vars */ + +declare module 'discord.js' { + export interface GuildMember { + /** + * Send a punishment dm to the user. + * @param punishment The punishment that the user has received. + * @param reason The reason for the user's punishment. + * @param duration The duration of the punishment. + * @param modlog The modlog case id so the user can make an appeal. + * @param sendFooter Whether or not to send the guild's punishment footer with the dm. + * @returns Whether or not the dm was sent successfully. + */ + bushPunishDM( + punishment: PunishmentTypeDM, + reason?: string | null, + duration?: number, + modlog?: string, + sendFooter?: boolean + ): Promise<boolean>; + /** + * Warn the user, create a modlog entry, and send a dm to the user. + * @param options Options for warning the user. + * @returns An object with the result of the warning, and the case number of the warn. + * @emits {@link BushClientEvents.bushWarn} + */ + bushWarn(options: BushPunishmentOptions): Promise<{ result: WarnResponse; caseNum: number | null }>; + /** + * Add a role to the user, if it is a punishment create a modlog entry, and create a punishment entry if it is temporary or a punishment. + * @param options Options for adding a role to the user. + * @returns A status message for adding the add. + * @emits {@link BushClientEvents.bushPunishRole} + */ + bushAddRole(options: AddRoleOptions): Promise<AddRoleResponse>; + /** + * Remove a role from the user, if it is a punishment create a modlog entry, and destroy a punishment entry if it was temporary or a punishment. + * @param options Options for removing a role from the user. + * @returns A status message for removing the role. + * @emits {@link BushClientEvents.bushPunishRoleRemove} + */ + bushRemoveRole(options: RemoveRoleOptions): Promise<RemoveRoleResponse>; + /** + * Mute the user, create a modlog entry, creates a punishment entry, and dms the user. + * @param options Options for muting the user. + * @returns A status message for muting the user. + * @emits {@link BushClientEvents.bushMute} + */ + bushMute(options: BushTimedPunishmentOptions): Promise<MuteResponse>; + /** + * Unmute the user, create a modlog entry, remove the punishment entry, and dm the user. + * @param options Options for unmuting the user. + * @returns A status message for unmuting the user. + * @emits {@link BushClientEvents.bushUnmute} + */ + bushUnmute(options: BushPunishmentOptions): Promise<UnmuteResponse>; + /** + * Kick the user, create a modlog entry, and dm the user. + * @param options Options for kicking the user. + * @returns A status message for kicking the user. + * @emits {@link BushClientEvents.bushKick} + */ + bushKick(options: BushPunishmentOptions): Promise<KickResponse>; + /** + * Ban the user, create a modlog entry, create a punishment entry, and dm the user. + * @param options Options for banning the user. + * @returns A status message for banning the user. + * @emits {@link BushClientEvents.bushBan} + */ + bushBan(options: BushBanOptions): Promise<Exclude<BanResponse, typeof banResponse['ALREADY_BANNED']>>; + /** + * Prevents a user from speaking in a channel. + * @param options Options for blocking the user. + */ + bushBlock(options: BlockOptions): Promise<BlockResponse>; + /** + * Allows a user to speak in a channel. + * @param options Options for unblocking the user. + */ + bushUnblock(options: UnblockOptions): Promise<UnblockResponse>; + /** + * Mutes a user using discord's timeout feature. + * @param options Options for timing out the user. + */ + bushTimeout(options: BushTimeoutOptions): Promise<TimeoutResponse>; + /** + * Removes a timeout from a user. + * @param options Options for removing the timeout. + */ + bushRemoveTimeout(options: BushPunishmentOptions): Promise<RemoveTimeoutResponse>; + /** + * Whether or not the user is an owner of the bot. + */ + isOwner(): boolean; + /** + * Whether or not the user is a super user of the bot. + */ + isSuperUser(): boolean; + } +} + +/** + * Represents a member of a guild on Discord. + */ +export class ExtendedGuildMember extends GuildMember { + /** + * Send a punishment dm to the user. + * @param punishment The punishment that the user has received. + * @param reason The reason for the user's punishment. + * @param duration The duration of the punishment. + * @param modlog The modlog case id so the user can make an appeal. + * @param sendFooter Whether or not to send the guild's punishment footer with the dm. + * @returns Whether or not the dm was sent successfully. + */ + public override async bushPunishDM( + punishment: PunishmentTypeDM, + reason?: string | null, + duration?: number, + modlog?: string, + sendFooter = true + ): Promise<boolean> { + return Moderation.punishDM({ + client: this.client, + modlog, + guild: this.guild, + user: this, + punishment, + reason: reason ?? undefined, + duration, + sendFooter + }); + } + + /** + * Warn the user, create a modlog entry, and send a dm to the user. + * @param options Options for warning the user. + * @returns An object with the result of the warning, and the case number of the warn. + * @emits {@link BushClientEvents.bushWarn} + */ + public override async bushWarn(options: BushPunishmentOptions): Promise<{ result: WarnResponse; caseNum: number | null }> { + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); + if (!moderator) return { result: warnResponse.CANNOT_RESOLVE_USER, caseNum: null }; + + const ret = await (async (): Promise<{ result: WarnResponse; caseNum: number | null }> => { + // add modlog entry + const result = await Moderation.createModLogEntry( + { + client: this.client, + type: ModLogType.WARN, + user: this, + moderator: moderator.id, + reason: options.reason, + guild: this.guild, + evidence: options.evidence, + hidden: options.silent ?? false + }, + true + ); + caseID = result.log?.id; + if (!result || !result.log) return { result: warnResponse.MODLOG_ERROR, caseNum: null }; + + if (!options.silent) { + // dm user + const dmSuccess = await this.bushPunishDM('warned', options.reason); + dmSuccessEvent = dmSuccess; + if (!dmSuccess) return { result: warnResponse.DM_ERROR, caseNum: result.caseNum }; + } + + return { result: warnResponse.SUCCESS, caseNum: result.caseNum }; + })(); + if (!([warnResponse.MODLOG_ERROR] as const).includes(ret.result) && !options.silent) + this.client.emit('bushWarn', this, moderator, this.guild, options.reason ?? undefined, caseID!, dmSuccessEvent!); + return ret; + } + + /** + * Add a role to the user, if it is a punishment create a modlog entry, and create a punishment entry if it is temporary or a punishment. + * @param options Options for adding a role to the user. + * @returns A status message for adding the add. + * @emits {@link BushClientEvents.bushPunishRole} + */ + public override async bushAddRole(options: AddRoleOptions): Promise<AddRoleResponse> { + // checks + if (!this.guild.members.me!.permissions.has(PermissionFlagsBits.ManageRoles)) return addRoleResponse.MISSING_PERMISSIONS; + const ifShouldAddRole = this.#checkIfShouldAddRole(options.role, options.moderator); + if (ifShouldAddRole !== true) return ifShouldAddRole; + + let caseID: string | undefined = undefined; + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); + if (!moderator) return addRoleResponse.CANNOT_RESOLVE_USER; + + const ret = await (async () => { + if (options.addToModlog || options.duration) { + const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, + type: options.duration ? ModLogType.TEMP_PUNISHMENT_ROLE : ModLogType.PERM_PUNISHMENT_ROLE, + guild: this.guild, + moderator: moderator.id, + user: this, + reason: 'N/A', + pseudo: !options.addToModlog, + evidence: options.evidence, + hidden: options.silent ?? false + }); + + if (!modlog) return addRoleResponse.MODLOG_ERROR; + caseID = modlog.id; + + if (options.addToModlog || options.duration) { + const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ + client: this.client, + type: 'role', + user: this, + guild: this.guild, + modlog: modlog.id, + duration: options.duration, + extraInfo: options.role.id + }); + if (!punishmentEntrySuccess) return addRoleResponse.PUNISHMENT_ENTRY_ADD_ERROR; + } + } + + const removeRoleSuccess = await this.roles.add(options.role, `${moderator.tag}`); + if (!removeRoleSuccess) return addRoleResponse.ACTION_ERROR; + + return addRoleResponse.SUCCESS; + })(); + if ( + !( + [addRoleResponse.ACTION_ERROR, addRoleResponse.MODLOG_ERROR, addRoleResponse.PUNISHMENT_ENTRY_ADD_ERROR] as const + ).includes(ret) && + options.addToModlog && + !options.silent + ) + this.client.emit( + 'bushPunishRole', + this, + moderator, + this.guild, + options.reason ?? undefined, + caseID!, + options.duration ?? 0, + options.role, + options.evidence + ); + return ret; + } + + /** + * Remove a role from the user, if it is a punishment create a modlog entry, and destroy a punishment entry if it was temporary or a punishment. + * @param options Options for removing a role from the user. + * @returns A status message for removing the role. + * @emits {@link BushClientEvents.bushPunishRoleRemove} + */ + public override async bushRemoveRole(options: RemoveRoleOptions): Promise<RemoveRoleResponse> { + // checks + if (!this.guild.members.me!.permissions.has(PermissionFlagsBits.ManageRoles)) return removeRoleResponse.MISSING_PERMISSIONS; + const ifShouldAddRole = this.#checkIfShouldAddRole(options.role, options.moderator); + if (ifShouldAddRole !== true) return ifShouldAddRole; + + let caseID: string | undefined = undefined; + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); + if (!moderator) return removeRoleResponse.CANNOT_RESOLVE_USER; + + const ret = await (async () => { + if (options.addToModlog) { + const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, + type: ModLogType.REMOVE_PUNISHMENT_ROLE, + guild: this.guild, + moderator: moderator.id, + user: this, + reason: 'N/A', + evidence: options.evidence, + hidden: options.silent ?? false + }); + + if (!modlog) return removeRoleResponse.MODLOG_ERROR; + caseID = modlog.id; + + const punishmentEntrySuccess = await Moderation.removePunishmentEntry({ + client: this.client, + type: 'role', + user: this, + guild: this.guild, + extraInfo: options.role.id + }); + + if (!punishmentEntrySuccess) return removeRoleResponse.PUNISHMENT_ENTRY_REMOVE_ERROR; + } + + const removeRoleSuccess = await this.roles.remove(options.role, `${moderator.tag}`); + if (!removeRoleSuccess) return removeRoleResponse.ACTION_ERROR; + + return removeRoleResponse.SUCCESS; + })(); + + if ( + !( + [ + removeRoleResponse.ACTION_ERROR, + removeRoleResponse.MODLOG_ERROR, + removeRoleResponse.PUNISHMENT_ENTRY_REMOVE_ERROR + ] as const + ).includes(ret) && + options.addToModlog && + !options.silent + ) + this.client.emit( + 'bushPunishRoleRemove', + this, + moderator, + this.guild, + options.reason ?? undefined, + caseID!, + options.role, + options.evidence + ); + return ret; + } + + /** + * Check whether or not a role should be added/removed from the user based on hierarchy. + * @param role The role to check if can be modified. + * @param moderator The moderator that is trying to add/remove the role. + * @returns `true` if the role should be added/removed or a string for the reason why it shouldn't. + */ + #checkIfShouldAddRole( + role: Role | Role, + moderator?: GuildMember + ): true | 'user hierarchy' | 'role managed' | 'client hierarchy' { + if (moderator && moderator.roles.highest.position <= role.position && this.guild.ownerId !== this.user.id) { + return shouldAddRoleResponse.USER_HIERARCHY; + } else if (role.managed) { + return shouldAddRoleResponse.ROLE_MANAGED; + } else if (this.guild.members.me!.roles.highest.position <= role.position) { + return shouldAddRoleResponse.CLIENT_HIERARCHY; + } + return true; + } + + /** + * Mute the user, create a modlog entry, creates a punishment entry, and dms the user. + * @param options Options for muting the user. + * @returns A status message for muting the user. + * @emits {@link BushClientEvents.bushMute} + */ + public override async bushMute(options: BushTimedPunishmentOptions): Promise<MuteResponse> { + // checks + const checks = await Moderation.checkMutePermissions(this.guild); + if (checks !== true) return checks; + + const muteRoleID = (await this.guild.getSetting('muteRole'))!; + const muteRole = this.guild.roles.cache.get(muteRoleID)!; + + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); + if (!moderator) return muteResponse.CANNOT_RESOLVE_USER; + + 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 this.client.console.warn('muteRoleAddError', e); + this.client.console.debug(e); + return false; + }); + if (!muteSuccess) return muteResponse.ACTION_ERROR; + + // add modlog entry + const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, + type: options.duration ? ModLogType.TEMP_MUTE : ModLogType.PERM_MUTE, + user: this, + moderator: moderator.id, + reason: options.reason, + duration: options.duration, + guild: this.guild, + evidence: options.evidence, + hidden: options.silent ?? false + }); + + if (!modlog) return muteResponse.MODLOG_ERROR; + caseID = modlog.id; + + // add punishment entry so they can be unmuted later + const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ + client: this.client, + type: 'mute', + user: this, + guild: this.guild, + duration: options.duration, + modlog: modlog.id + }); + + if (!punishmentEntrySuccess) return muteResponse.PUNISHMENT_ENTRY_ADD_ERROR; + + if (!options.silent) { + // dm user + const dmSuccess = await this.bushPunishDM('muted', options.reason, options.duration ?? 0, modlog.id); + dmSuccessEvent = dmSuccess; + if (!dmSuccess) return muteResponse.DM_ERROR; + } + + return muteResponse.SUCCESS; + })(); + + if ( + !([muteResponse.ACTION_ERROR, muteResponse.MODLOG_ERROR, muteResponse.PUNISHMENT_ENTRY_ADD_ERROR] as const).includes(ret) && + !options.silent + ) + this.client.emit( + 'bushMute', + this, + moderator, + this.guild, + options.reason ?? undefined, + caseID!, + options.duration ?? 0, + dmSuccessEvent!, + options.evidence + ); + return ret; + } + + /** + * Unmute the user, create a modlog entry, remove the punishment entry, and dm the user. + * @param options Options for unmuting the user. + * @returns A status message for unmuting the user. + * @emits {@link BushClientEvents.bushUnmute} + */ + public override async bushUnmute(options: BushPunishmentOptions): Promise<UnmuteResponse> { + // checks + const checks = await Moderation.checkMutePermissions(this.guild); + if (checks !== true) return checks; + + const muteRoleID = (await this.guild.getSetting('muteRole'))!; + const muteRole = this.guild.roles.cache.get(muteRoleID)!; + + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); + if (!moderator) return unmuteResponse.CANNOT_RESOLVE_USER; + + 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 this.client.console.warn('muteRoleAddError', formatError(e, true)); + return false; + }); + if (!muteSuccess) return unmuteResponse.ACTION_ERROR; + + // add modlog entry + const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, + type: ModLogType.UNMUTE, + user: this, + moderator: moderator.id, + reason: options.reason, + guild: this.guild, + evidence: options.evidence, + hidden: options.silent ?? false + }); + + if (!modlog) return unmuteResponse.MODLOG_ERROR; + caseID = modlog.id; + + // remove mute entry + const removePunishmentEntrySuccess = await Moderation.removePunishmentEntry({ + client: this.client, + type: 'mute', + user: this, + guild: this.guild + }); + + if (!removePunishmentEntrySuccess) return unmuteResponse.PUNISHMENT_ENTRY_REMOVE_ERROR; + + if (!options.silent) { + // dm user + const dmSuccess = await this.bushPunishDM('unmuted', options.reason, undefined, '', false); + dmSuccessEvent = dmSuccess; + if (!dmSuccess) return unmuteResponse.DM_ERROR; + } + + return unmuteResponse.SUCCESS; + })(); + + if ( + !( + [unmuteResponse.ACTION_ERROR, unmuteResponse.MODLOG_ERROR, unmuteResponse.PUNISHMENT_ENTRY_REMOVE_ERROR] as const + ).includes(ret) && + !options.silent + ) + this.client.emit( + 'bushUnmute', + this, + moderator, + this.guild, + options.reason ?? undefined, + caseID!, + dmSuccessEvent!, + options.evidence + ); + return ret; + } + + /** + * Kick the user, create a modlog entry, and dm the user. + * @param options Options for kicking the user. + * @returns A status message for kicking the user. + * @emits {@link BushClientEvents.bushKick} + */ + public override async bushKick(options: BushPunishmentOptions): Promise<KickResponse> { + // checks + if (!this.guild.members.me?.permissions.has(PermissionFlagsBits.KickMembers) || !this.kickable) + return kickResponse.MISSING_PERMISSIONS; + + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); + if (!moderator) return kickResponse.CANNOT_RESOLVE_USER; + const ret = await (async () => { + // add modlog entry + const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, + type: ModLogType.KICK, + user: this, + moderator: moderator.id, + reason: options.reason, + guild: this.guild, + evidence: options.evidence, + hidden: options.silent ?? false + }); + if (!modlog) return kickResponse.MODLOG_ERROR; + caseID = modlog.id; + + // dm user + const dmSuccess = options.silent ? null : await this.bushPunishDM('kicked', options.reason, undefined, modlog.id); + dmSuccessEvent = dmSuccess ?? undefined; + + // kick + const kickSuccess = await this.kick(`${moderator?.tag} | ${options.reason ?? 'No reason provided.'}`).catch(() => false); + if (!kickSuccess) return kickResponse.ACTION_ERROR; + + if (dmSuccess === false) return kickResponse.DM_ERROR; + return kickResponse.SUCCESS; + })(); + if (!([kickResponse.ACTION_ERROR, kickResponse.MODLOG_ERROR] as const).includes(ret) && !options.silent) + this.client.emit( + 'bushKick', + this, + moderator, + this.guild, + options.reason ?? undefined, + caseID!, + dmSuccessEvent!, + options.evidence + ); + return ret; + } + + /** + * Ban the user, create a modlog entry, create a punishment entry, and dm the user. + * @param options Options for banning the user. + * @returns A status message for banning the user. + * @emits {@link BushClientEvents.bushBan} + */ + public override async bushBan(options: BushBanOptions): Promise<Exclude<BanResponse, typeof banResponse['ALREADY_BANNED']>> { + // checks + if (!this.guild.members.me!.permissions.has(PermissionFlagsBits.BanMembers) || !this.bannable) + return banResponse.MISSING_PERMISSIONS; + + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); + if (!moderator) return banResponse.CANNOT_RESOLVE_USER; + + // ignore result, they should still be banned even if their mute cannot be removed + await this.bushUnmute({ + reason: 'User is about to be banned, a mute is no longer necessary.', + moderator: this.guild.members.me!, + silent: true + }); + + const ret = await (async () => { + // add modlog entry + const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, + type: options.duration ? ModLogType.TEMP_BAN : ModLogType.PERM_BAN, + user: this, + moderator: moderator.id, + reason: options.reason, + duration: options.duration, + guild: this.guild, + evidence: options.evidence, + hidden: options.silent ?? false + }); + if (!modlog) return banResponse.MODLOG_ERROR; + caseID = modlog.id; + + // dm user + const dmSuccess = options.silent + ? null + : await this.bushPunishDM('banned', options.reason, options.duration ?? 0, modlog.id); + dmSuccessEvent = dmSuccess ?? undefined; + + // ban + const banSuccess = await this.ban({ + reason: `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`, + 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({ + client: this.client, + type: 'ban', + user: this, + guild: this.guild, + duration: options.duration, + modlog: modlog.id + }); + if (!punishmentEntrySuccess) return banResponse.PUNISHMENT_ENTRY_ADD_ERROR; + + if (!dmSuccess) return banResponse.DM_ERROR; + return banResponse.SUCCESS; + })(); + if ( + !([banResponse.ACTION_ERROR, banResponse.MODLOG_ERROR, banResponse.PUNISHMENT_ENTRY_ADD_ERROR] as const).includes(ret) && + !options.silent + ) + this.client.emit( + 'bushBan', + this, + moderator, + this.guild, + options.reason ?? undefined, + caseID!, + options.duration ?? 0, + dmSuccessEvent!, + options.evidence + ); + return ret; + } + + /** + * Prevents a user from speaking in a channel. + * @param options Options for blocking the user. + */ + public override async bushBlock(options: BlockOptions): Promise<BlockResponse> { + const channel = this.guild.channels.resolve(options.channel); + if (!channel || (!channel.isTextBased() && !channel.isThread())) return blockResponse.INVALID_CHANNEL; + + // checks + if (!channel.permissionsFor(this.guild.members.me!)!.has(PermissionFlagsBits.ManageChannels)) + return blockResponse.MISSING_PERMISSIONS; + + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); + if (!moderator) return blockResponse.CANNOT_RESOLVE_USER; + + const ret = await (async () => { + // change channel permissions + const channelToUse = channel.isThread() ? channel.parent! : channel; + const perm = channel.isThread() ? { SendMessagesInThreads: false } : { SendMessages: false }; + const blockSuccess = await channelToUse.permissionOverwrites + .edit(this, perm, { reason: `[Block] ${moderator.tag} | ${options.reason ?? 'No reason provided.'}` }) + .catch(() => false); + if (!blockSuccess) return blockResponse.ACTION_ERROR; + + // add modlog entry + const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, + 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, + hidden: options.silent ?? false + }); + if (!modlog) return blockResponse.MODLOG_ERROR; + caseID = modlog.id; + + // add punishment entry so they can be unblocked later + const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ + client: this.client, + type: 'block', + user: this, + guild: this.guild, + duration: options.duration, + modlog: modlog.id, + extraInfo: channel.id + }); + if (!punishmentEntrySuccess) return blockResponse.PUNISHMENT_ENTRY_ADD_ERROR; + + // dm user + const dmSuccess = options.silent + ? null + : await Moderation.punishDM({ + client: this.client, + punishment: 'blocked', + reason: options.reason ?? undefined, + duration: options.duration ?? 0, + modlog: modlog.id, + guild: this.guild, + user: this, + sendFooter: true, + channel: channel.id + }); + dmSuccessEvent = !!dmSuccess; + if (!dmSuccess) return blockResponse.DM_ERROR; + + return blockResponse.SUCCESS; + })(); + + if ( + !([blockResponse.ACTION_ERROR, blockResponse.MODLOG_ERROR, blockResponse.PUNISHMENT_ENTRY_ADD_ERROR] as const).includes( + ret + ) && + !options.silent + ) + this.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 override async bushUnblock(options: UnblockOptions): Promise<UnblockResponse> { + const _channel = this.guild.channels.resolve(options.channel); + if (!_channel || (_channel.type !== ChannelType.GuildText && !_channel.isThread())) return unblockResponse.INVALID_CHANNEL; + const channel = _channel as GuildTextBasedChannel; + + // checks + if (!channel.permissionsFor(this.guild.members.me!)!.has(PermissionFlagsBits.ManageChannels)) + return unblockResponse.MISSING_PERMISSIONS; + + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); + if (!moderator) return unblockResponse.CANNOT_RESOLVE_USER; + + const ret = await (async () => { + // change channel permissions + const channelToUse = channel.isThread() ? channel.parent! : channel; + const perm = channel.isThread() ? { SendMessagesInThreads: null } : { SendMessages: null }; + const blockSuccess = await channelToUse.permissionOverwrites + .edit(this, perm, { reason: `[Unblock] ${moderator.tag} | ${options.reason ?? 'No reason provided.'}` }) + .catch(() => false); + if (!blockSuccess) return unblockResponse.ACTION_ERROR; + + // add modlog entry + const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, + type: ModLogType.CHANNEL_UNBLOCK, + user: this, + moderator: moderator.id, + reason: options.reason, + guild: this.guild, + evidence: options.evidence, + hidden: options.silent ?? false + }); + if (!modlog) return unblockResponse.MODLOG_ERROR; + caseID = modlog.id; + + // remove punishment entry + const punishmentEntrySuccess = await Moderation.removePunishmentEntry({ + client: this.client, + type: 'block', + user: this, + guild: this.guild, + extraInfo: channel.id + }); + if (!punishmentEntrySuccess) return unblockResponse.ACTION_ERROR; + + // dm user + const dmSuccess = options.silent + ? null + : await Moderation.punishDM({ + client: this.client, + punishment: 'unblocked', + reason: options.reason ?? undefined, + guild: this.guild, + user: this, + sendFooter: false, + channel: channel.id + }); + dmSuccessEvent = !!dmSuccess; + if (!dmSuccess) return blockResponse.DM_ERROR; + + dmSuccessEvent = !!dmSuccess; + if (!dmSuccess) return unblockResponse.DM_ERROR; + + return unblockResponse.SUCCESS; + })(); + + if ( + !([unblockResponse.ACTION_ERROR, unblockResponse.MODLOG_ERROR, unblockResponse.ACTION_ERROR] as const).includes(ret) && + !options.silent + ) + this.client.emit( + 'bushUnblock', + this, + moderator, + this.guild, + options.reason ?? undefined, + caseID!, + dmSuccessEvent!, + channel, + options.evidence + ); + return ret; + } + + /** + * Mutes a user using discord's timeout feature. + * @param options Options for timing out the user. + */ + public override async bushTimeout(options: BushTimeoutOptions): Promise<TimeoutResponse> { + // checks + if (!this.guild.members.me!.permissions.has(PermissionFlagsBits.ModerateMembers)) return timeoutResponse.MISSING_PERMISSIONS; + + const twentyEightDays = Time.Day * 28; + if (options.duration > twentyEightDays) return timeoutResponse.INVALID_DURATION; + + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); + if (!moderator) return timeoutResponse.CANNOT_RESOLVE_USER; + + const ret = await (async () => { + // timeout + const timeoutSuccess = await this.timeout( + options.duration, + `${moderator.tag} | ${options.reason ?? 'No reason provided.'}` + ).catch(() => false); + if (!timeoutSuccess) return timeoutResponse.ACTION_ERROR; + + // add modlog entry + const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, + type: ModLogType.TIMEOUT, + user: this, + moderator: moderator.id, + reason: options.reason, + duration: options.duration, + guild: this.guild, + evidence: options.evidence, + hidden: options.silent ?? false + }); + + if (!modlog) return timeoutResponse.MODLOG_ERROR; + caseID = modlog.id; + + if (!options.silent) { + // dm user + const dmSuccess = await this.bushPunishDM('timedout', options.reason, options.duration, modlog.id); + dmSuccessEvent = dmSuccess; + if (!dmSuccess) return timeoutResponse.DM_ERROR; + } + + return timeoutResponse.SUCCESS; + })(); + + if (!([timeoutResponse.ACTION_ERROR, timeoutResponse.MODLOG_ERROR] as const).includes(ret) && !options.silent) + this.client.emit( + 'bushTimeout', + this, + moderator, + this.guild, + options.reason ?? undefined, + caseID!, + options.duration ?? 0, + dmSuccessEvent!, + options.evidence + ); + return ret; + } + + /** + * Removes a timeout from a user. + * @param options Options for removing the timeout. + */ + public override async bushRemoveTimeout(options: BushPunishmentOptions): Promise<RemoveTimeoutResponse> { + // checks + if (!this.guild.members.me!.permissions.has(PermissionFlagsBits.ModerateMembers)) + return removeTimeoutResponse.MISSING_PERMISSIONS; + + let caseID: string | undefined = undefined; + let dmSuccessEvent: boolean | undefined = undefined; + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); + if (!moderator) return removeTimeoutResponse.CANNOT_RESOLVE_USER; + + const ret = await (async () => { + // remove timeout + const timeoutSuccess = await this.timeout(null, `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`).catch( + () => false + ); + if (!timeoutSuccess) return removeTimeoutResponse.ACTION_ERROR; + + // add modlog entry + const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, + type: ModLogType.REMOVE_TIMEOUT, + user: this, + moderator: moderator.id, + reason: options.reason, + guild: this.guild, + evidence: options.evidence, + hidden: options.silent ?? false + }); + + if (!modlog) return removeTimeoutResponse.MODLOG_ERROR; + caseID = modlog.id; + + if (!options.silent) { + // dm user + const dmSuccess = await this.bushPunishDM('untimedout', options.reason, undefined, '', false); + dmSuccessEvent = dmSuccess; + if (!dmSuccess) return removeTimeoutResponse.DM_ERROR; + } + + return removeTimeoutResponse.SUCCESS; + })(); + + if (!([removeTimeoutResponse.ACTION_ERROR, removeTimeoutResponse.MODLOG_ERROR] as const).includes(ret) && !options.silent) + this.client.emit( + 'bushRemoveTimeout', + this, + moderator, + this.guild, + options.reason ?? undefined, + caseID!, + dmSuccessEvent!, + options.evidence + ); + return ret; + } + + /** + * Whether or not the user is an owner of the bot. + */ + public override isOwner(): boolean { + return this.client.isOwner(this); + } + + /** + * Whether or not the user is a super user of the bot. + */ + public override isSuperUser(): boolean { + return this.client.isSuperUser(this); + } +} + +/** + * Options for punishing a user. + */ +export interface BushPunishmentOptions { + /** + * The reason for the punishment. + */ + reason?: string | null; + + /** + * The moderator who punished the user. + */ + moderator?: GuildMember; + + /** + * Evidence for the punishment. + */ + evidence?: string; + + /** + * Makes the punishment silent by not sending the user a punishment dm and not broadcasting the event to be logged. + */ + silent?: boolean; +} + +/** + * Punishment options for punishments that can be temporary. + */ +export interface BushTimedPunishmentOptions extends BushPunishmentOptions { + /** + * The duration of the punishment. + */ + duration?: number; +} + +/** + * Options for a role add punishment. + */ +export interface AddRoleOptions extends BushTimedPunishmentOptions { + /** + * The role to add to the user. + */ + role: Role; + + /** + * Whether to create a modlog entry for this punishment. + */ + addToModlog: boolean; +} + +/** + * Options for a role remove punishment. + */ +export interface RemoveRoleOptions extends BushTimedPunishmentOptions { + /** + * The role to remove from the user. + */ + role: Role; + + /** + * Whether to create a modlog entry for this punishment. + */ + addToModlog: boolean; +} + +/** + * Options for banning a user. + */ +export interface BushBanOptions extends BushTimedPunishmentOptions { + /** + * The number of days to delete the user's messages for. + */ + deleteDays?: number; +} + +/** + * Options for blocking a user from a channel. + */ +export interface BlockOptions extends BushTimedPunishmentOptions { + /** + * The channel to block the user from. + */ + channel: GuildChannelResolvable; +} + +/** + * Options for unblocking a user from a channel. + */ +export interface UnblockOptions extends BushPunishmentOptions { + /** + * The channel to unblock the user from. + */ + channel: GuildChannelResolvable; +} + +/** + * Punishment options for punishments that can be temporary. + */ +export interface BushTimeoutOptions extends BushPunishmentOptions { + /** + * The duration of the punishment. + */ + duration: number; +} + +export const basePunishmentResponse = Object.freeze({ + SUCCESS: 'success', + MODLOG_ERROR: 'error creating modlog entry', + ACTION_ERROR: 'error performing action', + CANNOT_RESOLVE_USER: 'cannot resolve user' +} as const); + +export const dmResponse = Object.freeze({ + ...basePunishmentResponse, + DM_ERROR: 'failed to dm' +} as const); + +export const permissionsResponse = Object.freeze({ + MISSING_PERMISSIONS: 'missing permissions' +} as const); + +export const punishmentEntryAdd = Object.freeze({ + PUNISHMENT_ENTRY_ADD_ERROR: 'error creating punishment entry' +} as const); + +export const punishmentEntryRemove = Object.freeze({ + PUNISHMENT_ENTRY_REMOVE_ERROR: 'error removing punishment entry' +} as const); + +export const shouldAddRoleResponse = Object.freeze({ + USER_HIERARCHY: 'user hierarchy', + CLIENT_HIERARCHY: 'client hierarchy', + ROLE_MANAGED: 'role managed' +} as const); + +export const baseBlockResponse = Object.freeze({ + INVALID_CHANNEL: 'invalid channel' +} as const); + +export const baseMuteResponse = Object.freeze({ + NO_MUTE_ROLE: 'no mute role', + MUTE_ROLE_INVALID: 'invalid mute role', + MUTE_ROLE_NOT_MANAGEABLE: 'mute role not manageable' +} as const); + +export const warnResponse = Object.freeze({ + ...dmResponse +} as const); + +export const addRoleResponse = Object.freeze({ + ...basePunishmentResponse, + ...permissionsResponse, + ...shouldAddRoleResponse, + ...punishmentEntryAdd +} as const); + +export const removeRoleResponse = Object.freeze({ + ...basePunishmentResponse, + ...permissionsResponse, + ...shouldAddRoleResponse, + ...punishmentEntryRemove +} as const); + +export const muteResponse = Object.freeze({ + ...dmResponse, + ...permissionsResponse, + ...baseMuteResponse, + ...punishmentEntryAdd +} as const); + +export const unmuteResponse = Object.freeze({ + ...dmResponse, + ...permissionsResponse, + ...baseMuteResponse, + ...punishmentEntryRemove +} as const); + +export const kickResponse = Object.freeze({ + ...dmResponse, + ...permissionsResponse +} as const); + +export const banResponse = Object.freeze({ + ...dmResponse, + ...permissionsResponse, + ...punishmentEntryAdd, + ALREADY_BANNED: 'already banned' +} as const); + +export const blockResponse = Object.freeze({ + ...dmResponse, + ...permissionsResponse, + ...baseBlockResponse, + ...punishmentEntryAdd +}); + +export const unblockResponse = Object.freeze({ + ...dmResponse, + ...permissionsResponse, + ...baseBlockResponse, + ...punishmentEntryRemove +}); + +export const timeoutResponse = Object.freeze({ + ...dmResponse, + ...permissionsResponse, + INVALID_DURATION: 'duration too long' +} as const); + +export const removeTimeoutResponse = Object.freeze({ + ...dmResponse, + ...permissionsResponse +} as const); + +/** + * Response returned when warning a user. + */ +export type WarnResponse = ValueOf<typeof warnResponse>; + +/** + * Response returned when adding a role to a user. + */ +export type AddRoleResponse = ValueOf<typeof addRoleResponse>; + +/** + * Response returned when removing a role from a user. + */ +export type RemoveRoleResponse = ValueOf<typeof removeRoleResponse>; + +/** + * Response returned when muting a user. + */ +export type MuteResponse = ValueOf<typeof muteResponse>; + +/** + * Response returned when unmuting a user. + */ +export type UnmuteResponse = ValueOf<typeof unmuteResponse>; + +/** + * Response returned when kicking a user. + */ +export type KickResponse = ValueOf<typeof kickResponse>; + +/** + * Response returned when banning a user. + */ +export type BanResponse = ValueOf<typeof banResponse>; + +/** + * Response returned when blocking a user. + */ +export type BlockResponse = ValueOf<typeof blockResponse>; + +/** + * Response returned when unblocking a user. + */ +export type UnblockResponse = ValueOf<typeof unblockResponse>; + +/** + * Response returned when timing out a user. + */ +export type TimeoutResponse = ValueOf<typeof timeoutResponse>; + +/** + * Response returned when removing a timeout from a user. + */ +export type RemoveTimeoutResponse = ValueOf<typeof removeTimeoutResponse>; + +/** + * @typedef {BushClientEvents} VSCodePleaseDontRemove + */ diff --git a/lib/extensions/discord.js/ExtendedMessage.ts b/lib/extensions/discord.js/ExtendedMessage.ts new file mode 100644 index 0000000..1bb0904 --- /dev/null +++ b/lib/extensions/discord.js/ExtendedMessage.ts @@ -0,0 +1,12 @@ +import { CommandUtil } from 'discord-akairo'; +import { Message, type Client } from 'discord.js'; +import type { RawMessageData } from 'discord.js/typings/rawDataTypes.js'; + +export class ExtendedMessage<Cached extends boolean = boolean> extends Message<Cached> { + public declare util: CommandUtil<Message>; + + public constructor(client: Client, data: RawMessageData) { + super(client, data); + this.util = new CommandUtil(client.commandHandler, this); + } +} diff --git a/lib/extensions/discord.js/ExtendedUser.ts b/lib/extensions/discord.js/ExtendedUser.ts new file mode 100644 index 0000000..23de523 --- /dev/null +++ b/lib/extensions/discord.js/ExtendedUser.ts @@ -0,0 +1,35 @@ +import { User, type Partialize } from 'discord.js'; + +declare module 'discord.js' { + export interface User { + /** + * Indicates whether the user is an owner of the bot. + */ + isOwner(): boolean; + /** + * Indicates whether the user is a superuser of the bot. + */ + isSuperUser(): boolean; + } +} + +export type PartialBushUser = Partialize<ExtendedUser, 'username' | 'tag' | 'discriminator' | 'isOwner' | 'isSuperUser'>; + +/** + * Represents a user on Discord. + */ +export class ExtendedUser extends User { + /** + * Indicates whether the user is an owner of the bot. + */ + public override isOwner(): boolean { + return this.client.isOwner(this); + } + + /** + * Indicates whether the user is a superuser of the bot. + */ + public override isSuperUser(): boolean { + return this.client.isSuperUser(this); + } +} |