diff options
Diffstat (limited to 'src')
47 files changed, 349 insertions, 328 deletions
diff --git a/src/arguments/globalUser.ts b/src/arguments/globalUser.ts index 69d3e89..08350d2 100644 --- a/src/arguments/globalUser.ts +++ b/src/arguments/globalUser.ts @@ -2,7 +2,5 @@ import { type BushArgumentTypeCaster, type BushUser } from '#lib'; // resolve non-cached users export const globalUser: BushArgumentTypeCaster<Promise<BushUser | null>> = async (_, phrase) => { - return client.users.cache.has(phrase) - ? client.users.cache.get(`${phrase}`) ?? null - : await client.users.fetch(`${phrase}`).catch(() => null); + return client.users.resolve(phrase) ?? (await client.users.fetch(`${phrase}`).catch(() => null)); }; diff --git a/src/arguments/roleWithDuration.ts b/src/arguments/roleWithDuration.ts index d619b9e..5f1da98 100644 --- a/src/arguments/roleWithDuration.ts +++ b/src/arguments/roleWithDuration.ts @@ -3,10 +3,10 @@ import { type Role } from 'discord.js'; export const roleWithDuration: BushArgumentTypeCaster<Promise<RoleWithDuration | null>> = async (message, phrase) => { // eslint-disable-next-line prefer-const - let { duration, contentWithoutTime } = client.util.parseDuration(phrase); - if (contentWithoutTime === null || contentWithoutTime === undefined) return null; - contentWithoutTime = contentWithoutTime.trim(); - const role = await util.arg.cast('role', message, contentWithoutTime); + let { duration, content } = client.util.parseDuration(phrase); + if (content === null || content === undefined) return null; + content = content.trim(); + const role = await util.arg.cast('role', message, content); if (!role) return null; return { duration, role }; }; diff --git a/src/commands/admin/channelPermissions.ts b/src/commands/admin/channelPermissions.ts index e12a131..37610b3 100644 --- a/src/commands/admin/channelPermissions.ts +++ b/src/commands/admin/channelPermissions.ts @@ -1,4 +1,5 @@ import { BushCommand, ButtonPaginator, type ArgType, type BushMessage, type BushSlashMessage } from '#lib'; +import assert from 'assert'; import { ApplicationCommandOptionType, Embed, PermissionFlagsBits } from 'discord.js'; export default class ChannelPermissionsCommand extends BushCommand { @@ -62,9 +63,7 @@ export default class ChannelPermissionsCommand extends BushCommand { state: 'true' | 'false' | 'neutral'; } ) { - if (!message.inGuild()) return await message.util.reply(`${util.emojis.error} This command can only be run in a server.`); - if (!message.member!.permissions.has(PermissionFlagsBits.Administrator) && !message.member!.user.isOwner()) - return await message.util.reply(`${util.emojis.error} You must have admin perms to use this command.`); + assert(message.inGuild()); if (message.util.isSlashMessage(message)) await message.interaction.deferReply(); const permission = message.util.isSlashMessage(message) @@ -72,7 +71,7 @@ export default class ChannelPermissionsCommand extends BushCommand { : args.permission; if (!permission) return await message.util.reply(`${util.emojis.error} Invalid permission.`); const failedChannels = []; - for (const [, channel] of message.guild!.channels.cache) { + for (const [, channel] of message.guild.channels.cache) { try { if (channel.isThread()) return; if (channel.permissionsLocked) return; diff --git a/src/commands/config/blacklist.ts b/src/commands/config/blacklist.ts index d210472..ba2d24a 100644 --- a/src/commands/config/blacklist.ts +++ b/src/commands/config/blacklist.ts @@ -1,4 +1,5 @@ import { AllowedMentions, BushCommand, type ArgType, type BushMessage, type BushSlashMessage } from '#lib'; +import assert from 'assert'; import { ApplicationCommandOptionType, PermissionFlagsBits, User } from 'discord.js'; export default class BlacklistCommand extends BushCommand { @@ -40,7 +41,6 @@ export default class BlacklistCommand extends BushCommand { } ], slash: true, - channel: 'guild', clientPermissions: (m) => util.clientSendAndPermCheck(m), userPermissions: [PermissionFlagsBits.ManageGuild] }); @@ -64,8 +64,11 @@ export default class BlacklistCommand extends BushCommand { if (!target) return await message.util.reply(`${util.emojis.error} Choose a valid channel or user.`); const targetID = target.id; - if (!message.guild && global) + if (!message.inGuild() && !global) return await message.util.reply(`${util.emojis.error} You have to be in a guild to disable commands.`); + + if (!global) assert(message.inGuild()); + const blacklistedUsers = global ? util.getGlobal('blacklistedUsers') : (await message.guild!.getSetting('blacklistedChannels')) ?? []; diff --git a/src/commands/config/config.ts b/src/commands/config/config.ts index 2fae2fd..f860b30 100644 --- a/src/commands/config/config.ts +++ b/src/commands/config/config.ts @@ -208,8 +208,10 @@ export default class ConfigCommand extends BushCommand { value: ArgType<'channel'> | ArgType<'role'> | string; } ) { - if (!message.guild) return await message.util.reply(`${util.emojis.error} This command can only be used in servers.`); - if (!message.member?.permissions.has(PermissionFlagsBits.ManageGuild) && !message.member?.user.isOwner()) + assert(message.inGuild()); + assert(message.member); + + if (!message.member.permissions.has(PermissionFlagsBits.ManageGuild) && !message.member?.user.isOwner()) return await message.util.reply(`${util.emojis.error} You must have the **Manage Server** permission to run this command.`); const setting = message.util.isSlash ? (_.camelCase(args.subcommandGroup)! as GuildSettings) : args.setting!; const action = message.util.isSlash ? args.subcommand! : args.action!; @@ -263,7 +265,8 @@ export default class ConfigCommand extends BushCommand { collector.on('collect', async (interaction: MessageComponentInteraction) => { if (interaction.user.id === message.author.id || client.config.owners.includes(interaction.user.id)) { - if (!message.guild) throw new Error('message.guild is null'); + assert(message.inGuild()); + switch (interaction.customId) { case 'command_settingsSel': { if (!interaction.isSelectMenu()) return; @@ -288,10 +291,11 @@ export default class ConfigCommand extends BushCommand { message: BushMessage | BushSlashMessage, setting?: undefined | keyof typeof guildSettingsObj ): Promise<MessageOptions & InteractionUpdateOptions> { - if (!message.guild) throw new Error('message.guild is null'); + assert(message.inGuild()); + const settingsEmbed = new Embed().setColor(util.colors.default); if (!setting) { - settingsEmbed.setTitle(`${message.guild!.name}'s Settings`); + settingsEmbed.setTitle(`${message.guild.name}'s Settings`); const desc = settingsArr.map((s) => `:wrench: **${guildSettingsObj[s].name}**`).join('\n'); settingsEmbed.setDescription(desc); @@ -314,7 +318,7 @@ export default class ConfigCommand extends BushCommand { } else { settingsEmbed.setTitle(guildSettingsObj[setting].name); const generateCurrentValue = async (type: GuildSettingType): Promise<string> => { - const feat = await message.guild!.getSetting(setting); + const feat = await message.guild.getSetting(setting); let func = (v: string) => v; switch (type.replace('-array', '') as BaseSettingTypes) { case 'string': { diff --git a/src/commands/config/disable.ts b/src/commands/config/disable.ts index a7ebfa2..db325fc 100644 --- a/src/commands/config/disable.ts +++ b/src/commands/config/disable.ts @@ -57,6 +57,8 @@ export default class DisableCommand extends BushCommand { message: BushMessage | BushSlashMessage, args: { action?: 'enable' | 'disable'; command: ArgType<'commandAlias'> | string; global: boolean } ) { + assert(message.inGuild()); + let action = (args.action ?? message?.util?.parsed?.alias ?? 'toggle') as 'disable' | 'enable' | 'toggle'; const global = args.global && message.author.isOwner(); const commandID = @@ -67,13 +69,13 @@ export default class DisableCommand extends BushCommand { if (DisableCommand.blacklistedCommands.includes(commandID)) return message.util.send(`${util.emojis.error} the ${commandID} command cannot be disabled.`); - const disabledCommands = global ? util.getGlobal('disabledCommands') : await message.guild!.getSetting('disabledCommands'); + const disabledCommands = global ? util.getGlobal('disabledCommands') : await message.guild.getSetting('disabledCommands'); if (action === 'toggle') action = disabledCommands.includes(commandID) ? 'disable' : 'enable'; const newValue = util.addOrRemoveFromArray(action === 'disable' ? 'remove' : 'add', disabledCommands, commandID); const success = global ? await util.setGlobal('disabledCommands', newValue).catch(() => false) - : await message.guild!.setSetting('disabledCommands', newValue, message.member!).catch(() => false); + : await message.guild.setSetting('disabledCommands', newValue, message.member!).catch(() => false); if (!success) return await message.util.reply({ content: `${util.emojis.error} There was an error${global ? ' globally' : ''} **${action.substring( diff --git a/src/commands/config/features.ts b/src/commands/config/features.ts index 199f201..c9aebd3 100644 --- a/src/commands/config/features.ts +++ b/src/commands/config/features.ts @@ -6,6 +6,7 @@ import { type BushSlashMessage, type GuildFeatures } from '#lib'; +import assert from 'assert'; import { ActionRow, ComponentType, @@ -33,11 +34,11 @@ export default class FeaturesCommand extends BushCommand { } public override async exec(message: BushMessage | BushSlashMessage) { - if (!message.guild) return await message.util.reply(`${util.emojis.error} This command can only be used in servers.`); + assert(message.inGuild()); - const featureEmbed = new Embed().setTitle(`${message.guild!.name}'s Features`).setColor(util.colors.default); + const featureEmbed = new Embed().setTitle(`${message.guild.name}'s Features`).setColor(util.colors.default); - const enabledFeatures = await message.guild!.getSetting('enabledFeatures'); + const enabledFeatures = await message.guild.getSetting('enabledFeatures'); this.generateDescription(guildFeaturesArr, enabledFeatures, featureEmbed); const components = this.generateComponents(guildFeaturesArr, false); const msg = (await message.util.reply({ embeds: [featureEmbed], components: [components] })) as Message; @@ -49,7 +50,7 @@ export default class FeaturesCommand extends BushCommand { collector.on('collect', async (interaction: SelectMenuInteraction) => { if (interaction.user.id === message.author.id || client.config.owners.includes(interaction.user.id)) { - if (!message.guild) throw new Error('message.guild is null'); + assert(message.inGuild()); const [selected]: GuildFeatures[] = interaction.values as GuildFeatures[]; diff --git a/src/commands/config/log.ts b/src/commands/config/log.ts index 79f9258..f99f007 100644 --- a/src/commands/config/log.ts +++ b/src/commands/config/log.ts @@ -1,4 +1,5 @@ import { BushCommand, guildLogsArr, type ArgType, type BushMessage, type BushSlashMessage, type GuildLogType } from '#lib'; +import assert from 'assert'; import { ArgumentGeneratorReturn } from 'discord-akairo'; import { ApplicationCommandOptionType, ChannelType, PermissionFlagsBits } from 'discord.js'; @@ -72,7 +73,8 @@ export default class LogCommand extends BushCommand { message: BushMessage | BushSlashMessage, args: { log_type: GuildLogType; channel: ArgType<'textChannel'> } ) { - if (!message.guild) return await message.util.reply(`${util.emojis.error} This command can only be used in servers.`); + assert(message.inGuild()); + const currentLogs = await message.guild.getSetting('logChannels'); const oldChannel = currentLogs[args.log_type] ?? undefined; diff --git a/src/commands/info/icon.ts b/src/commands/info/icon.ts index 2b5b8fb..72e82d8 100644 --- a/src/commands/info/icon.ts +++ b/src/commands/info/icon.ts @@ -1,4 +1,5 @@ import { BushCommand, type BushMessage, type BushSlashMessage } from '#lib'; +import assert from 'assert'; import { Embed, PermissionFlagsBits } from 'discord.js'; export default class IconCommand extends BushCommand { @@ -17,16 +18,18 @@ export default class IconCommand extends BushCommand { } public override async exec(message: BushMessage | BushSlashMessage) { + assert(message.inGuild()); + const embed = new Embed() .setTimestamp() .setColor(util.colors.default) .setImage( - message.guild!.iconURL({ + message.guild.iconURL({ size: 2048, extension: 'png' })! ) - .setTitle(util.discord.escapeMarkdown(message.guild!.name)); + .setTitle(util.discord.escapeMarkdown(message.guild.name)); await message.util.reply({ embeds: [embed] }); } } diff --git a/src/commands/info/links.ts b/src/commands/info/links.ts index 25b040c..d91f1e7 100644 --- a/src/commands/info/links.ts +++ b/src/commands/info/links.ts @@ -1,5 +1,5 @@ import { BushCommand, type BushMessage, type BushSlashMessage } from '#lib'; -import { assert } from 'console'; +import assert from 'assert'; import { ActionRow, ButtonComponent, ButtonStyle } from 'discord.js'; import packageDotJSON from '../../../package.json' assert { type: 'json' }; diff --git a/src/commands/leveling/leaderboard.ts b/src/commands/leveling/leaderboard.ts index eb8b90c..0871811 100644 --- a/src/commands/leveling/leaderboard.ts +++ b/src/commands/leveling/leaderboard.ts @@ -1,4 +1,5 @@ import { BushCommand, ButtonPaginator, Level, type ArgType, type BushMessage, type BushSlashMessage } from '#lib'; +import assert from 'assert'; import { ApplicationCommandOptionType, Embed, PermissionFlagsBits } from 'discord.js'; export default class LeaderboardCommand extends BushCommand { @@ -28,7 +29,8 @@ export default class LeaderboardCommand extends BushCommand { } public override async exec(message: BushMessage | BushSlashMessage, args: { page: ArgType<'integer'> }) { - if (!message.guild) return await message.util.reply(`${util.emojis.error} This command can only be run in a server.`); + assert(message.inGuild()); + if (!(await message.guild.hasFeature('leveling'))) return await message.util.reply( `${util.emojis.error} This command can only be run in servers with the leveling feature enabled.${ @@ -43,7 +45,7 @@ export default class LeaderboardCommand extends BushCommand { (val, index) => `\`${index + 1}\` <@${val.user}> - Level ${val.level} (${val.xp.toLocaleString()} xp)` ); const chunked = util.chunk(mappedRanks, 25); - const embeds = chunked.map((c) => new Embed().setTitle(`${message.guild!.name}'s Leaderboard`).setDescription(c.join('\n'))); + const embeds = chunked.map((c) => new Embed().setTitle(`${message.guild.name}'s Leaderboard`).setDescription(c.join('\n'))); return await ButtonPaginator.send(message, embeds, undefined, true, args?.page ?? undefined); } } diff --git a/src/commands/leveling/level.ts b/src/commands/leveling/level.ts index 271c3f6..803703e 100644 --- a/src/commands/leveling/level.ts +++ b/src/commands/leveling/level.ts @@ -47,7 +47,8 @@ export default class LevelCommand extends BushCommand { } public override async exec(message: BushMessage | BushSlashMessage, args: { user: OptionalArgType<'user'> }) { - if (!message.guild) return await message.util.reply(`${util.emojis.error} This command can only be run in a server.`); + assert(message.inGuild()); + if (!(await message.guild.hasFeature('leveling'))) return await message.util.reply( `${util.emojis.error} This command can only be run in servers with the leveling feature enabled.${ @@ -59,7 +60,7 @@ export default class LevelCommand extends BushCommand { const user = args.user ?? message.author; try { return await message.util.reply({ - files: [new MessageAttachment(await this.getImage(user, message.guild!), 'level.png')] + files: [new MessageAttachment(await this.getImage(user, message.guild), 'level.png')] }); } catch (e) { if (e instanceof Error && e.message === 'User does not have a level') { diff --git a/src/commands/leveling/setLevel.ts b/src/commands/leveling/setLevel.ts index 1016280..e74d885 100644 --- a/src/commands/leveling/setLevel.ts +++ b/src/commands/leveling/setLevel.ts @@ -1,4 +1,5 @@ import { AllowedMentions, BushCommand, Level, type ArgType, type BushMessage, type BushSlashMessage } from '#lib'; +import assert from 'assert'; import { ApplicationCommandOptionType, PermissionFlagsBits } from 'discord.js'; export default class SetLevelCommand extends BushCommand { @@ -38,10 +39,11 @@ export default class SetLevelCommand extends BushCommand { message: BushMessage | BushSlashMessage, { user, level }: { user: ArgType<'user'>; level: ArgType<'integer'> } ) { - if (!message.guild) return await message.util.reply(`${util.emojis.error} This command can only be run in a guild.`); - if (!user.id) throw new Error('user.id is null'); + assert(message.inGuild()); + assert(user.id); - if (isNaN(level)) return await message.util.reply(`${util.emojis.error} Provide a valid number to set the user's level to.`); + if (isNaN(level) || !Number.isInteger(level)) + return await message.util.reply(`${util.emojis.error} Provide a valid number to set the user's level to.`); if (level > 6553 || level < 0) return await message.util.reply(`${util.emojis.error} You cannot set a level higher than \`6553\`.`); diff --git a/src/commands/leveling/setXp.ts b/src/commands/leveling/setXp.ts index a86c58a..7b1b432 100644 --- a/src/commands/leveling/setXp.ts +++ b/src/commands/leveling/setXp.ts @@ -1,4 +1,5 @@ import { AllowedMentions, BushCommand, Level, type ArgType, type BushMessage, type BushSlashMessage } from '#lib'; +import assert from 'assert'; import { ApplicationCommandOptionType, PermissionFlagsBits } from 'discord.js'; export default class SetXpCommand extends BushCommand { @@ -39,8 +40,8 @@ export default class SetXpCommand extends BushCommand { message: BushMessage | BushSlashMessage, { user, xp }: { user: ArgType<'user'>; xp: ArgType<'abbreviatedNumber'> } ) { - if (!message.guild) return await message.util.reply(`${util.emojis.error} This command can only be run in a guild.`); - if (!user.id) throw new Error('user.id is null'); + assert(message.inGuild()); + assert(user.id); if (isNaN(xp)) return await message.util.reply(`${util.emojis.error} Provide a valid number.`); if (xp > 2147483647 || xp < 0) diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 5e3350f..25102e0 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -8,7 +8,8 @@ import { type BushSlashMessage, type OptionalArgType } from '#lib'; -import { ApplicationCommandOptionType, PermissionFlagsBits, type User } from 'discord.js'; +import assert from 'assert'; +import { ApplicationCommandOptionType, PermissionFlagsBits } from 'discord.js'; export default class BanCommand extends BushCommand { public constructor() { @@ -71,20 +72,21 @@ export default class BanCommand extends BushCommand { message: BushMessage | BushSlashMessage, args: { user: ArgType<'user'> | ArgType<'snowflake'>; - reason_and_duration: OptionalArgType<'contentWithDuration'>; + reason_and_duration: OptionalArgType<'contentWithDuration'> | string; days: OptionalArgType<'integer'>; force: boolean; } ) { - if (args.reason_and_duration && typeof args.reason_and_duration === 'object') args.reason_and_duration.duration ??= 0; - args.days ??= 0; + assert(message.inGuild()); + assert(message.member); - if (!message.guild) return message.util.reply(`${util.emojis.error} This command cannot be used in dms.`); - const member = message.guild!.members.cache.get((args.user as User)?.id ?? args.user); - const user = member?.user ?? (await util.resolveNonCachedUser((args.user as User)?.id ?? args.user)); + const { duration, content } = await util.castDurationContent(args.reason_and_duration, message); + + args.days ??= message.util.parsed?.alias === 'dban' ? 1 : 0; + const member = message.guild.members.cache.get(typeof args.user === 'string' ? args.user : args.user.id); + const user = member?.user ?? (await util.resolveNonCachedUser(typeof args.user === 'string' ? args.user : args.user.id)); if (!user) return message.util.reply(`${util.emojis.error} Invalid user.`); const useForce = args.force && message.author.isOwner(); - if (!message.member) throw new Error(`message.member is null`); const canModerateResponse = member ? await Moderation.permissionCheck(message.member, member, 'ban', true, useForce) : true; @@ -92,35 +94,13 @@ export default class BanCommand extends BushCommand { return await message.util.reply(canModerateResponse); } - if (message.util.parsed?.alias === 'dban' && !args.days) args.days = 1; - if (!Number.isInteger(args.days) || args.days! < 0 || args.days! > 7) { return message.util.reply(`${util.emojis.error} The delete days must be an integer between 0 and 7.`); } - let time: number | null; - if (args.reason_and_duration) { - time = - typeof args.reason_and_duration === 'string' - ? await util.arg.cast('duration', message, args.reason_and_duration) - : args.reason_and_duration.duration; - } - const parsedReason = args.reason_and_duration?.contentWithoutTime ?? null; + const opts = { reason: content, moderator: message.member, duration: duration, deleteDays: args.days }; - const responseCode = member - ? await member.bushBan({ - reason: parsedReason, - moderator: message.member, - duration: time! ?? 0, - deleteDays: args.days - }) - : await message.guild.bushBan({ - user, - reason: parsedReason, - moderator: message.member, - duration: time! ?? 0, - deleteDays: args.days - }); + const responseCode = member ? await member.bushBan(opts) : await message.guild.bushBan({ user, ...opts }); const responseMessage = (): string => { const victim = util.format.input(user.tag); diff --git a/src/commands/moderation/block.ts b/src/commands/moderation/block.ts index 20c6e86..e6f7849 100644 --- a/src/commands/moderation/block.ts +++ b/src/commands/moderation/block.ts @@ -2,8 +2,6 @@ import { AllowedMentions, blockResponse, BushCommand, - BushTextChannel, - BushThreadChannel, Moderation, type ArgType, type BushMessage, @@ -67,21 +65,17 @@ export default class BlockCommand extends BushCommand { } ) { 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.`); + assert(message.member); + + if (!message.channel.isTextBased()) + return message.util.send(`${util.emojis.error} This command can only be used in text based channels.`); - const reason = args.reason_and_duration - ? typeof args.reason_and_duration === 'string' - ? await util.arg.cast('contentWithDuration', message, args.reason_and_duration) - : args.reason_and_duration - : { duration: null, contentWithoutTime: '' }; + const { duration, content } = await util.castDurationContent(args.reason_and_duration, message); - if (reason.duration === null) reason.duration = 0; - const member = await message.guild!.members.fetch(args.user.id).catch(() => null); + 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); @@ -89,18 +83,10 @@ export default class BlockCommand extends BushCommand { 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.bushBlock({ - reason: parsedReason, + reason: content, moderator: message.member, - duration: time ?? 0, + duration: duration, channel: message.channel }); diff --git a/src/commands/moderation/evidence.ts b/src/commands/moderation/evidence.ts index 23ccf59..d189e89 100644 --- a/src/commands/moderation/evidence.ts +++ b/src/commands/moderation/evidence.ts @@ -1,4 +1,5 @@ import { BushCommand, ModLog, type BushMessage, type BushSlashMessage } from '#lib'; +import assert from 'assert'; import { ArgumentGeneratorReturn } from 'discord-akairo'; import { ArgumentTypeCasterReturn } from 'discord-akairo/dist/src/struct/commands/arguments/Argument.js'; import { ApplicationCommandOptionType, PermissionFlagsBits } from 'discord.js'; @@ -65,9 +66,11 @@ export default class EvidenceCommand extends BushCommand { message: BushMessage | BushSlashMessage, { case_id: caseID, evidence }: { case_id: string; evidence?: string } ) { + assert(message.inGuild()); + const entry = await ModLog.findByPk(caseID); if (!entry || entry.pseudo) return message.util.send(`${util.emojis.error} Invalid modlog entry.`); - if (entry.guild !== message.guild!.id) return message.util.reply(`${util.emojis.error} This modlog is from another server.`); + if (entry.guild !== message.guild.id) return message.util.reply(`${util.emojis.error} This modlog is from another server.`); if (evidence && (message as BushMessage).attachments?.size) return message.util.reply(`${util.emojis.error} Please either attach an image or a reason not both.`); diff --git a/src/commands/moderation/hideCase.ts b/src/commands/moderation/hideCase.ts index a59380f..d603953 100644 --- a/src/commands/moderation/hideCase.ts +++ b/src/commands/moderation/hideCase.ts @@ -1,4 +1,5 @@ import { BushCommand, ModLog, type BushMessage, type BushSlashMessage } from '#lib'; +import assert from 'assert'; import { ApplicationCommandOptionType, PermissionFlagsBits } from 'discord.js'; export default class HideCaseCommand extends BushCommand { @@ -27,9 +28,11 @@ export default class HideCaseCommand extends BushCommand { } public override async exec(message: BushMessage | BushSlashMessage, { case_id: caseID }: { case_id: string }) { + assert(message.inGuild()); + const entry = await ModLog.findByPk(caseID); if (!entry || entry.pseudo) return message.util.send(`${util.emojis.error} Invalid entry.`); - if (entry.guild !== message.guild!.id) return message.util.reply(`${util.emojis.error} This modlog is from another server.`); + if (entry.guild !== message.guild.id) return message.util.reply(`${util.emojis.error} This modlog is from another server.`); const action = entry.hidden ? 'no longer hidden' : 'now hidden'; const oldEntry = entry.hidden; entry.hidden = !entry.hidden; diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index 6dfb09b..26098b5 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -7,6 +7,7 @@ import { type BushMessage, type BushSlashMessage } from '#lib'; +import assert from 'assert'; import { ApplicationCommandOptionType, PermissionFlagsBits } from 'discord.js'; export default class KickCommand extends BushCommand { @@ -57,11 +58,13 @@ export default class KickCommand extends BushCommand { message: BushMessage | BushSlashMessage, { user, reason, force }: { user: ArgType<'user'>; reason: ArgType<'string'>; force: boolean } ) { - const member = await message.guild!.members.fetch(user.id); + assert(message.inGuild()); + assert(message.member); + + const member = await message.guild.members.fetch(user.id); 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.`); - if (!message.member) throw new Error(`message.member is null`); const useForce = force && message.author.isOwner(); const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'kick', true, useForce); diff --git a/src/commands/moderation/massBan.ts b/src/commands/moderation/massBan.ts index 5621011..e4aeb55 100644 --- a/src/commands/moderation/massBan.ts +++ b/src/commands/moderation/massBan.ts @@ -56,6 +56,9 @@ export default class MassBanCommand extends BushCommand { args: { users: ArgType<'string'>; reason: OptionalArgType<'string'>; days: OptionalArgType<'integer'> } ) { assert(message.inGuild()); + + args.days ??= message.util.parsed?.alias?.includes('dban') ? 1 : 0; + const ids = args.users.split(/\n| /).filter((id) => id.length > 0); if (ids.length === 0) return message.util.send(`${util.emojis.error} You must provide at least one user id.`); for (const id of ids) { @@ -67,8 +70,6 @@ export default class MassBanCommand extends BushCommand { return message.util.reply(`${util.emojis.error} The delete days must be an integer between 0 and 7.`); } - if (message.util.parsed?.alias?.includes('dban') && !args.days) args.days = 1; - const promises = ids.map((id) => message.guild.bushBan({ user: id, diff --git a/src/commands/moderation/modlog.ts b/src/commands/moderation/modlog.ts index 443ffa3..c089d40 100644 --- a/src/commands/moderation/modlog.ts +++ b/src/commands/moderation/modlog.ts @@ -1,4 +1,5 @@ import { BushCommand, ButtonPaginator, ModLog, type ArgType, type BushMessage, type BushSlashMessage } from '#lib'; +import assert from 'assert'; import { ApplicationCommandOptionType, Embed, PermissionFlagsBits, User } from 'discord.js'; export default class ModlogCommand extends BushCommand { @@ -30,6 +31,7 @@ export default class ModlogCommand extends BushCommand { } ], slash: true, + channel: 'guild', clientPermissions: (m) => util.clientSendAndPermCheck(m), userPermissions: (m) => util.userGuildPermCheck(m, [PermissionFlagsBits.ManageMessages]) }); @@ -39,11 +41,13 @@ export default class ModlogCommand extends BushCommand { message: BushMessage | BushSlashMessage, { search, hidden }: { search: ArgType<'user'> | string; hidden: boolean } ) { + assert(message.inGuild()); + const foundUser = search instanceof User ? search : await util.resolveUserAsync(search); if (foundUser) { const logs = await ModLog.findAll({ where: { - guild: message.guild!.id, + guild: message.guild.id, user: foundUser.id }, order: [['createdAt', 'ASC']] @@ -68,8 +72,7 @@ export default class ModlogCommand extends BushCommand { const entry = await ModLog.findByPk(search as string); if (!entry || entry.pseudo || (entry.hidden && !hidden)) return message.util.send(`${util.emojis.error} That modlog does not exist.`); - if (entry.guild !== message.guild!.id) - return message.util.reply(`${util.emojis.error} This modlog is from another server.`); + if (entry.guild !== message.guild.id) return message.util.reply(`${util.emojis.error} This modlog is from another server.`); const embed = { title: `Case ${entry.id}`, description: ModlogCommand.generateModlogInfo(entry, true), diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index c97ceb7..e32ece2 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -64,18 +64,15 @@ export default class MuteCommand extends BushCommand { force?: ArgType<'boolean'>; } ) { - const reason = args.reason_and_duration - ? typeof args.reason_and_duration === 'string' - ? await util.arg.cast('contentWithDuration', message, args.reason_and_duration) - : args.reason_and_duration - : { duration: null, contentWithoutTime: '' }; + assert(message.inGuild()); + assert(message.member); + + const { duration, content } = await util.castDurationContent(args.reason_and_duration, message); - if (reason.duration === null) reason.duration = 0; - const member = await message.guild!.members.fetch(args.user.id).catch(() => null); + 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, 'mute', true, useForce); @@ -83,18 +80,10 @@ export default class MuteCommand extends BushCommand { 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.bushMute({ - reason: parsedReason, + reason: content, moderator: message.member, - duration: time ?? 0 + duration }); const responseMessage = (): string => { diff --git a/src/commands/moderation/purge.ts b/src/commands/moderation/purge.ts index c882b7f..2cc9d04 100644 --- a/src/commands/moderation/purge.ts +++ b/src/commands/moderation/purge.ts @@ -53,8 +53,8 @@ export default class PurgeCommand extends BushCommand { message: BushMessage | BushSlashMessage, args: { amount: number; bot: boolean; user: ArgType<'user'> } ) { - assert(message.channel); - if (!message.inGuild()) return message.util.reply(`${util.emojis.error} You cannot run this command in dms.`); + assert(message.inGuild()); + if (args.amount > 100 || args.amount < 1) return message.util.reply(`${util.emojis.error} `); const messageFilter = (filterMessage: BushMessage): boolean => { @@ -67,13 +67,12 @@ export default class PurgeCommand extends BushCommand { const _messages = (await message.channel.messages.fetch({ limit: 100, before: message.id })) .filter((message) => messageFilter(message)) .first(args.amount); - const messages = new Collection<Snowflake, BushMessage>(); - _messages.forEach((m) => messages.set(m.id, m)); + const messages = new Collection<Snowflake, BushMessage>(_messages.map((m) => [m.id, m])); const purged = await message.channel.bulkDelete(messages, true).catch(() => null); if (!purged) return message.util.reply(`${util.emojis.error} Failed to purge messages.`).catch(() => null); else { - client.emit('bushPurge', message.author, message.guild!, message.channel, messages); + client.emit('bushPurge', message.author, message.guild, message.channel, messages); await message.util.send(`${util.emojis.success} Successfully purged **${purged.size}** messages.`); /* .then(async (purgeMessage) => { if (!message.util.isSlashMessage(message)) { diff --git a/src/commands/moderation/removeReactionEmoji.ts b/src/commands/moderation/removeReactionEmoji.ts index 092b8ac..f511007 100644 --- a/src/commands/moderation/removeReactionEmoji.ts +++ b/src/commands/moderation/removeReactionEmoji.ts @@ -1,6 +1,6 @@ import { BushCommand, type ArgType, type BushMessage, type BushSlashMessage } from '#lib'; import assert from 'assert'; -import { ApplicationCommandOptionType, Message, PermissionFlagsBits, type Emoji } from 'discord.js'; +import { ApplicationCommandOptionType, Message, PermissionFlagsBits } from 'discord.js'; export default class RemoveReactionEmojiCommand extends BushCommand { public constructor() { @@ -45,23 +45,19 @@ export default class RemoveReactionEmojiCommand extends BushCommand { assert(message.channel); const resolvedMessage = args.message instanceof Message ? args.message : await message.channel.messages.fetch(args.message); - const id = !(['string'] as const).includes(typeof args.emoji); - const emojiID = !id ? `${args.emoji}` : (args.emoji as Emoji).id; - const success = await resolvedMessage.reactions.cache + const emojiID = typeof args.emoji === 'string' ? `${args.emoji}` : args.emoji.id; + const success = !!(await resolvedMessage.reactions.cache ?.get(emojiID!) ?.remove() - ?.catch(() => {}); + ?.catch(() => undefined)); + if (success) { return await message.util.reply( - `${util.emojis.success} Removed all reactions of \`${id ? emojiID : args.emoji}\` from the message with the id of \`${ - resolvedMessage.id - }\`.` + `${util.emojis.success} Removed all reactions of \`${emojiID}\` from the message with the id of \`${resolvedMessage.id}\`.` ); } else { return await message.util.reply( - `${util.emojis.error} There was an error removing all reactions of \`${ - id ? emojiID : args.emoji - }\` from the message with the id of \`${resolvedMessage.id}\`.` + `${util.emojis.error} There was an error removing all reactions of \`${emojiID}\` from the message with the id of \`${resolvedMessage.id}\`.` ); } } diff --git a/src/commands/moderation/role.ts b/src/commands/moderation/role.ts index 920ef81..8580f2f 100644 --- a/src/commands/moderation/role.ts +++ b/src/commands/moderation/role.ts @@ -128,7 +128,7 @@ export default class RoleCommand extends BushCommand { } ) { if (!args.role) return await message.util.reply(`${util.emojis.error} You must specify a role.`); - if (args.duration === null) args.duration = 0; + args.duration ??= 0; if ( !message.member!.permissions.has(PermissionFlagsBits.ManageRoles) && message.member!.id !== message.guild?.ownerId && @@ -162,20 +162,12 @@ export default class RoleCommand extends BushCommand { const shouldLog = this.punishmentRoleNames.includes(args.role.name); - const responseCode = - args.action === 'add' - ? await args.member.bushAddRole({ - moderator: message.member!, - addToModlog: shouldLog, - role: args.role, - duration: args.duration - }) - : await args.member.bushRemoveRole({ - moderator: message.member!, - addToModlog: shouldLog, - role: args.role, - duration: args.duration - }); + const responseCode = await args.member[`bush${args.action === 'add' ? 'Add' : 'Remove'}Role`]({ + moderator: message.member!, + addToModlog: shouldLog, + role: args.role, + duration: args.duration + }); const responseMessage = (): string => { const victim = util.format.input(args.member.user.tag); diff --git a/src/commands/moderation/slowmode.ts b/src/commands/moderation/slowmode.ts index fb446d1..c0511f3 100644 --- a/src/commands/moderation/slowmode.ts +++ b/src/commands/moderation/slowmode.ts @@ -1,6 +1,7 @@ import { BushCommand, type ArgType, type BushMessage, type BushSlashMessage } from '#lib'; +import assert from 'assert'; import { Argument } from 'discord-akairo'; -import { ApplicationCommandOptionType, ChannelType, PermissionFlagsBits, type TextChannel, type ThreadChannel } from 'discord.js'; +import { ApplicationCommandOptionType, ChannelType, PermissionFlagsBits } from 'discord.js'; export default class SlowmodeCommand extends BushCommand { public constructor() { @@ -42,38 +43,33 @@ export default class SlowmodeCommand extends BushCommand { public override async exec( message: BushMessage | BushSlashMessage, - { - length, - channel - }: { + args: { length: ArgType<'duration'> | ArgType<'durationSeconds'> | 'off' | 'none' | 'disable' | null; channel: ArgType<'channel'>; } ) { - if (message.channel!.type === ChannelType.DM) - return await message.util.reply(`${util.emojis.error} This command cannot be run in dms.`); - if (!channel) channel = message.channel as any; - if (![ChannelType.GuildText, ChannelType.GuildPrivateThread, ChannelType.GuildPublicThread].includes(channel.type)) - return await message.util.reply(`${util.emojis.error} <#${channel.id}> is not a text or thread channel.`); - if (length) { - length = - typeof length === 'string' && !(['off', 'none', 'disable'] as const).includes(length) - ? await util.arg.cast('duration', message, length) - : length; - } + assert(message.inGuild()); + + args.channel ??= message.channel; + + if (!args.channel.isTextBased() || args.channel.isNews()) + return await message.util.reply(`${util.emojis.error} <#${args.channel.id}> is not a text or thread channel.`); - const length2: number = (['off', 'none', 'disable'] as const).includes(length as string) ? 0 : (length as number); + args.length = + typeof args.length === 'string' && !(['off', 'none', 'disable'] as const).includes(args.length) + ? await util.arg.cast('duration', message, args.length) + : args.length; - const setSlowmode = await (channel as ThreadChannel | TextChannel) + const length2: number = (['off', 'none', 'disable'] as const).includes(args.length) || args.length === null ? 0 : args.length; + + const setSlowmode = await args.channel .setRateLimitPerUser(length2 / 1000, `Changed by ${message.author.tag} (${message.author.id}).`) .catch(() => {}); if (!setSlowmode) - return await message.util.reply( - `${util.emojis.error} There was an error changing the slowmode of <#${(channel as ThreadChannel | TextChannel).id}>.` - ); + return await message.util.reply(`${util.emojis.error} There was an error changing the slowmode of <#${args.channel.id}>.`); else return await message.util.reply( - `${util.emojis.success} Successfully changed the slowmode of <#${channel.id}> ${ + `${util.emojis.success} Successfully changed the slowmode of <#${args.channel.id}> ${ length2 ? `to \`${util.humanizeDuration(length2)}` : '`off' }\`.` ); diff --git a/src/commands/moderation/timeout.ts b/src/commands/moderation/timeout.ts index 32fcf76..b8fb78f 100644 --- a/src/commands/moderation/timeout.ts +++ b/src/commands/moderation/timeout.ts @@ -58,19 +58,16 @@ export default class TimeoutCommand extends BushCommand { message: BushMessage | BushSlashMessage, args: { user: ArgType<'user'>; reason_and_duration: ArgType<'contentWithDuration'> | string; force?: ArgType<'boolean'> } ) { - const reason = args.reason_and_duration - ? typeof args.reason_and_duration === 'string' - ? await util.arg.cast('contentWithDuration', message, args.reason_and_duration) - : args.reason_and_duration - : { duration: null, contentWithoutTime: '' }; + assert(message.inGuild()); + assert(message.member); + + const { duration, content } = await util.castDurationContent(args.reason_and_duration, message); - if (reason.duration === null || reason.duration < 1) - return await message.util.reply(`${util.emojis.error} You must specify a duration for timeouts.`); - const member = await message.guild!.members.fetch(args.user.id).catch(() => null); + if (!duration) return await message.util.reply(`${util.emojis.error} You must specify a duration for timeouts.`); + 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, 'timeout', true, useForce); @@ -78,13 +75,10 @@ export default class TimeoutCommand extends BushCommand { return message.util.reply(canModerateResponse); } - const time = reason.duration; - const parsedReason = reason.contentWithoutTime ?? ''; - const responseCode = await member.bushTimeout({ - reason: parsedReason, + reason: content, moderator: message.member, - duration: time + duration: duration }); const responseMessage = (): string => { diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/unban.ts index 7bdb32e..a6afc0a 100644 --- a/src/commands/moderation/unban.ts +++ b/src/commands/moderation/unban.ts @@ -7,6 +7,7 @@ import { type BushSlashMessage, type OptionalArgType } from '#lib'; +import assert from 'assert'; import { ApplicationCommandOptionType, PermissionFlagsBits } from 'discord.js'; export default class UnbanCommand extends BushCommand { @@ -21,7 +22,7 @@ export default class UnbanCommand extends BushCommand { { id: 'user', description: 'The user to unban.', - type: 'globalUser', + type: util.arg.compose('user', 'globalUser'), prompt: 'What user would you like to unban?', retry: '{error} Choose a valid user to unban.', slashType: ApplicationCommandOptionType.User @@ -48,7 +49,9 @@ export default class UnbanCommand extends BushCommand { message: BushMessage | BushSlashMessage, { user, reason }: { user: ArgType<'user'>; reason: OptionalArgType<'string'> } ) { - const responseCode = await message.guild!.bushUnban({ + assert(message.inGuild()); + + const responseCode = await message.guild.bushUnban({ user, moderator: message.author, reason diff --git a/src/commands/moderation/unblock.ts b/src/commands/moderation/unblock.ts index 7d36e15..34b2075 100644 --- a/src/commands/moderation/unblock.ts +++ b/src/commands/moderation/unblock.ts @@ -1,8 +1,6 @@ import { AllowedMentions, BushCommand, - BushTextChannel, - BushThreadChannel, Moderation, unblockResponse, type ArgType, @@ -63,14 +61,15 @@ export default class UnblockCommand extends BushCommand { 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.`); + assert(message.member); + + if (!message.channel.isTextBased()) + return message.util.send(`${util.emojis.error} This command can only be used in text based channels.`); - const member = await message.guild!.members.fetch(args.user.id).catch(() => null); + 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); @@ -78,10 +77,8 @@ export default class UnblockCommand extends BushCommand { return message.util.reply(canModerateResponse); } - const parsedReason = args.reason ?? ''; - const responseCode = await member.bushUnblock({ - reason: parsedReason, + reason: args.reason ?? '', moderator: message.member, channel: message.channel }); diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts index fb4bb55..de16cb5 100644 --- a/src/commands/moderation/unmute.ts +++ b/src/commands/moderation/unmute.ts @@ -9,6 +9,7 @@ import { type BushSlashMessage, type OptionalArgType } from '#lib'; +import assert from 'assert'; import { ApplicationCommandOptionType, PermissionFlagsBits } from 'discord.js'; export default class UnmuteCommand extends BushCommand { @@ -60,10 +61,11 @@ export default class UnmuteCommand extends BushCommand { message: BushMessage | BushSlashMessage, { user, reason, force = false }: { user: ArgType<'user'>; reason: OptionalArgType<'string'>; force?: boolean } ) { - const error = util.emojis.error; - const member = message.guild!.members.cache.get(user.id) as BushGuildMember; + assert(message.inGuild()); + assert(message.member); - if (!message.member) throw new Error(`message.member is null`); + const error = util.emojis.error; + const member = message.guild.members.cache.get(user.id) as BushGuildMember; const useForce = force && message.author.isOwner(); const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'unmute', true, useForce); diff --git a/src/commands/moderation/untimeout.ts b/src/commands/moderation/untimeout.ts index 6d3632d..636b178 100644 --- a/src/commands/moderation/untimeout.ts +++ b/src/commands/moderation/untimeout.ts @@ -60,9 +60,10 @@ export default class UntimeoutCommand extends BushCommand { message: BushMessage | BushSlashMessage, args: { user: ArgType<'user'>; reason: OptionalArgType<'string'>; force?: ArgType<'boolean'> } ) { + assert(message.inGuild()); assert(message.member); - const member = await message.guild!.members.fetch(args.user.id).catch(() => null); + 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.`); diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index 27d04b3..3ab4b0b 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -4,11 +4,11 @@ import { Moderation, warnResponse, type ArgType, - type BushGuildMember, type BushMessage, type BushSlashMessage, type OptionalArgType } from '#lib'; +import assert from 'assert'; import { ApplicationCommandOptionType, PermissionFlagsBits } from 'discord.js'; export default class WarnCommand extends BushCommand { @@ -59,10 +59,12 @@ export default class WarnCommand extends BushCommand { message: BushMessage | BushSlashMessage, { user, reason, force = false }: { user: ArgType<'user'>; reason: OptionalArgType<'string'>; force?: boolean } ) { - const member = message.guild!.members.cache.get(user.id) as BushGuildMember; + assert(message.inGuild()); + assert(message.member); + + const member = message.guild.members.cache.get(user.id); if (!member) return message.util.reply(`${util.emojis.error} I cannot warn users that are not in the server.`); const useForce = force && message.author.isOwner(); - if (!message.member) throw new Error(`message.member is null`); const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'warn', true, useForce); if (canModerateResponse !== true) { diff --git a/src/commands/moulberry-bush/capePermissions.ts b/src/commands/moulberry-bush/capePermissions.ts index e568036..edb2836 100644 --- a/src/commands/moulberry-bush/capePermissions.ts +++ b/src/commands/moulberry-bush/capePermissions.ts @@ -28,21 +28,13 @@ export default class CapePermissionsCommand extends BushCommand { } public override async exec(message: BushMessage | BushSlashMessage, args: { ign: ArgType<'string'> }) { - interface CapePerms { - success: boolean; - perms: User[]; - } - - interface User { - _id: string; - perms: string[]; - } - let capePerms: CapePerms | null, uuid: string; try { uuid = await util.mcUUID(args.ign); } catch (e) { - return await message.util.reply(`${util.emojis.error} \`${args.ign}\` doesn't appear to be a valid username.`); + return await message.util.reply( + `${util.emojis.error} ${util.format.input(args.ign)} doesn't appear to be a valid username.` + ); } try { @@ -77,3 +69,13 @@ export default class CapePermissionsCommand extends BushCommand { } } } + +interface CapePerms { + success: boolean; + perms: User[]; +} + +interface User { + _id: string; + perms: string[]; +} diff --git a/src/commands/moulberry-bush/capes.ts b/src/commands/moulberry-bush/capes.ts index 47a4ea6..551a5c6 100644 --- a/src/commands/moulberry-bush/capes.ts +++ b/src/commands/moulberry-bush/capes.ts @@ -38,49 +38,32 @@ export default class CapesCommand extends BushCommand { const { tree: neuFileTree }: GithubTreeApi = await got .get('https://api.github.com/repos/Moulberry/NotEnoughUpdates/git/trees/master?recursive=1') .json(); - const capes = neuFileTree + const rawCapes = neuFileTree .map((f) => ({ match: f.path.match(/src\/main\/resources\/assets\/notenoughupdates\/capes\/(?<name>\w+)_preview\.png/), f })) .filter((f) => f.match !== null); - const capes1: { name: string; url: string; index: number; purchasable?: boolean }[] = []; - client.consts.mappings.capes.forEach((mapCape) => { - if (!capes.some((gitCape) => gitCape.match!.groups!.name === mapCape.name) && mapCape.custom) { - capes1.push({ - name: mapCape.name, - url: mapCape.custom, - index: mapCape.index, - purchasable: mapCape.purchasable - }); - } - }); - capes.forEach((gitCape) => { - const mapCape = client.consts.mappings.capes.find((a) => a.name === gitCape.match!.groups!.name); - const url = mapCape?.custom ?? `https://github.com/Moulberry/NotEnoughUpdates/raw/master/${gitCape.f.path}`; - const index = mapCape?.index !== undefined ? mapCape.index : null; - capes1.push({ name: gitCape.match!.groups!.name, url, index: index!, purchasable: mapCape?.purchasable }); - }); - - const sortedCapes = capes1.sort((a, b) => { - let aWeight: number | undefined = undefined, - bWeight: number | undefined = undefined; - aWeight ??= a?.index; - bWeight ??= b?.index; - - if (aWeight !== undefined && bWeight !== undefined) { - return aWeight - bWeight; - } else if (aWeight === undefined) { - return 1; - } else if (bWeight === undefined) { - return -1; - } + const capes: { name: string; url: string; index: number; purchasable?: boolean }[] = [ + ...client.consts.mappings.capes + .filter((c) => !rawCapes.some((gitCape) => gitCape.match!.groups!.name === c.name) && c.custom) + .map((c) => ({ name: c.name, url: c.custom!, index: c.index, purchasable: c.purchasable })), + ...rawCapes.map((c) => { + const mapCape = client.consts.mappings.capes.find((a) => a.name === c.match!.groups!.name); + const url = mapCape?.custom ?? `https://github.com/Moulberry/NotEnoughUpdates/raw/master/${c.f.path}`; + const index = mapCape?.index !== undefined ? mapCape.index : null; + return { name: c.match!.groups!.name, url, index: index!, purchasable: mapCape?.purchasable }; + }) + ].sort((a, b) => { + if (a?.index !== undefined && b.index !== undefined) return a.index - b?.index; + else if (a.index === undefined) return 1; + else if (b.index === undefined) return -1; return 0; }); if (args.cape) { - const cape = sortedCapes.find((s_cape) => s_cape.name === args.cape); + const cape = capes.find((s_cape) => s_cape.name === args.cape); if (cape) { const embed = this.makeEmbed(cape); await DeleteButton.send(message, { embeds: [embed] }); @@ -88,7 +71,7 @@ export default class CapesCommand extends BushCommand { await message.util.reply(`${util.emojis.error} Cannot find a cape called \`${args.cape}\`.`); } } else { - const embeds: APIEmbed[] = sortedCapes.map(this.makeEmbed); + const embeds: APIEmbed[] = capes.map(this.makeEmbed); await ButtonPaginator.send(message, embeds, null); } } diff --git a/src/commands/moulberry-bush/report.ts b/src/commands/moulberry-bush/report.ts index becdeeb..fc78a7b 100644 --- a/src/commands/moulberry-bush/report.ts +++ b/src/commands/moulberry-bush/report.ts @@ -1,4 +1,5 @@ import { AllowedMentions, BushCommand, type ArgType, type BushMessage } from '#lib'; +import assert from 'assert'; import { ApplicationCommandOptionType, Embed, PermissionFlagsBits } from 'discord.js'; export default class ReportCommand extends BushCommand { @@ -37,7 +38,9 @@ export default class ReportCommand extends BushCommand { } public override async exec(message: BushMessage, { member, evidence }: { member: ArgType<'member'>; evidence: string }) { - if (!message.guild || !(await message.guild.hasFeature('reporting'))) + assert(message.inGuild()); + + if (!(await message.guild.hasFeature('reporting'))) return await message.util.reply( `${util.emojis.error} This command can only be used in servers where reporting is enabled.` ); diff --git a/src/commands/utilities/activity.ts b/src/commands/utilities/activity.ts index 882c15d..230cc81 100644 --- a/src/commands/utilities/activity.ts +++ b/src/commands/utilities/activity.ts @@ -155,8 +155,7 @@ export default class ActivityCommand extends BushCommand { args: { channel: ArgType<'voiceChannel'>; activity: string } ) { const channel = typeof args.channel === 'string' ? message.guild?.channels.cache.get(args.channel) : args.channel; - if (!channel || channel.type !== ChannelType.GuildVoice) - return await message.util.reply(`${util.emojis.error} Choose a valid voice channel`); + if (!channel || !channel.isVoice()) return await message.util.reply(`${util.emojis.error} Choose a valid voice channel`); const target_application_id = message.util.isSlashMessage(message) ? args.activity diff --git a/src/commands/utilities/remind.ts b/src/commands/utilities/remind.ts index 4b7ccb9..044d4fc 100644 --- a/src/commands/utilities/remind.ts +++ b/src/commands/utilities/remind.ts @@ -1,4 +1,4 @@ -import { BushCommand, Reminder, type ArgType, type BushMessage, type BushSlashMessage } from '#lib'; +import { BushCommand, Reminder, Time, type ArgType, type BushMessage, type BushSlashMessage } from '#lib'; import { ApplicationCommandOptionType } from 'discord.js'; export default class RemindCommand extends BushCommand { @@ -7,11 +7,11 @@ export default class RemindCommand extends BushCommand { aliases: ['remind', 'remindme', 'reminder'], category: 'utilities', description: 'Create reminders that will be DMed to you when the time expires.', - usage: ['remind <duration> <reason>'], + usage: ['remind <duration> <reminder>'], examples: ['template 1 2'], args: [ { - id: 'reason_and_duration', + id: 'reminder', type: 'contentWithDuration', match: 'rest', description: 'The reason to be reminded and the duration to remind the user in.', @@ -29,32 +29,30 @@ export default class RemindCommand extends BushCommand { public override async exec( message: BushMessage | BushSlashMessage, - args: { reason_and_duration: ArgType<'contentWithDuration'> | string } + args: { reminder: ArgType<'contentWithDuration'> | string } ) { - const { duration, contentWithoutTime: reason } = - typeof args.reason_and_duration === 'string' - ? await util.arg.cast('contentWithDuration', message, args.reason_and_duration) - : args.reason_and_duration; + const { duration, content } = await util.castDurationContent(args.reminder, message); - if (!reason?.trim()) return await message.util.reply(`${util.emojis.error} Please enter a reason to be reminded about.`); - if (!duration) return await message.util.reply(`${util.emojis.error} Please enter a duration.`); + if (!content.trim()) return await message.util.reply(`${util.emojis.error} Please enter a reason to be reminded about.`); + if (!duration) return await message.util.reply(`${util.emojis.error} Please enter a time to remind you in.`); - if (duration < 30_000) - return await message.util.reply(`${util.emojis.error} You cannot pick a duration less than 30 seconds.`); + if (duration < Time.Second * 30) + return await message.util.reply(`${util.emojis.error} You cannot be reminded in less than 30 seconds.`); - const created = new Date(); const expires = new Date(Date.now() + duration); - const delta = util.format.bold(util.dateDelta(expires)); const success = await Reminder.create({ - content: reason.trim(), + content: content.trim(), messageUrl: message.url!, user: message.author.id, - created, - expires + created: new Date(), + expires: expires }).catch(() => false); if (!success) return await message.util.reply(`${util.emojis.error} Could not create a reminder.`); + + // This isn't technically accurate, but it prevents it from being .99 seconds + const delta = util.format.bold(util.dateDelta(new Date(Date.now() + duration))); return await message.util.reply(`${util.emojis.success} I will remind you in ${delta} (${util.timestamp(expires, 'T')}).`); } } diff --git a/src/commands/utilities/viewRaw.ts b/src/commands/utilities/viewRaw.ts index ee57e2d..be79499 100644 --- a/src/commands/utilities/viewRaw.ts +++ b/src/commands/utilities/viewRaw.ts @@ -1,13 +1,6 @@ -import { - BushCommand, - type ArgType, - type BushMessage, - type BushNewsChannel, - type BushSlashMessage, - type BushTextChannel, - type OptionalArgType -} from '#lib'; -import { ApplicationCommandOptionType, ChannelType, Embed, Message, PermissionFlagsBits, type Snowflake } from 'discord.js'; +import { BushCommand, type ArgType, type BushMessage, type BushSlashMessage, type OptionalArgType } from '#lib'; +import assert from 'assert'; +import { ApplicationCommandOptionType, ChannelType, Embed, Message, PermissionFlagsBits } from 'discord.js'; export default class ViewRawCommand extends BushCommand { public constructor() { @@ -30,7 +23,7 @@ export default class ViewRawCommand extends BushCommand { { id: 'channel', description: 'The channel that the message is in.', - type: util.arg.union('textChannel', 'newsChannel'), + type: util.arg.union('textChannel', 'newsChannel', 'threadChannel'), prompt: 'What channel is the message in?', retry: '{error} Choose a valid channel.', optional: true, @@ -74,22 +67,23 @@ export default class ViewRawCommand extends BushCommand { message: BushMessage | BushSlashMessage, args: { message: ArgType<'guildMessage'> | ArgType<'messageLink'>; - channel: OptionalArgType<'textChannel'> | OptionalArgType<'newsChannel'>; + channel: OptionalArgType<'textChannel'> | OptionalArgType<'newsChannel'> | OptionalArgType<'threadChannel'>; json: boolean; js: boolean; } ) { - if (!args.channel) args.channel = (message.channel as BushTextChannel | BushNewsChannel)!; + assert(message.inGuild()); + + args.channel ??= message.channel; + const newMessage = - args.message instanceof Message - ? args.message - : await args.channel.messages.fetch(`${args.message}` as Snowflake).catch(() => null); + args.message instanceof Message ? args.message : await args.channel!.messages.fetch(`${args.message}`).catch(() => null); if (!newMessage) return await message.util.reply( `${util.emojis.error} There was an error fetching that message, make sure that is a valid id and if the message is not in this channel, please provide a channel.` ); - const Embed = await ViewRawCommand.getRawData(newMessage as BushMessage, { json: args.json, js: args.js }); + const Embed = await ViewRawCommand.getRawData(newMessage, { json: args.json, js: args.js }); return await message.util.reply({ embeds: [Embed] }); } diff --git a/src/lib/common/AutoMod.ts b/src/lib/common/AutoMod.ts index fc5532b..3b22935 100644 --- a/src/lib/common/AutoMod.ts +++ b/src/lib/common/AutoMod.ts @@ -178,6 +178,7 @@ export class AutoMod { } private async checkPerspectiveApi() { + return; if (!client.config.isDevelopment) return; if (!this.message.content) return; diff --git a/src/lib/extensions/discord-akairo/BushClient.ts b/src/lib/extensions/discord-akairo/BushClient.ts index 43ae139..e46e701 100644 --- a/src/lib/extensions/discord-akairo/BushClient.ts +++ b/src/lib/extensions/discord-akairo/BushClient.ts @@ -42,6 +42,7 @@ import { } from 'discord.js'; import EventEmitter from 'events'; import { google } from 'googleapis'; +import snakeCase from 'lodash.snakecase'; import path from 'path'; import readline from 'readline'; import type { Options as SequelizeOptions, Sequelize as SequelizeType } from 'sequelize'; @@ -213,7 +214,9 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re allowedMentions: AllowedMentions.users(), // No everyone or role mentions by default makeCache: Options.cacheWithLimits({}), failIfNotExists: false, - rest: { api: 'https://canary.discord.com/api' } + rest: { api: 'https://canary.discord.com/api' }, + // todo: remove this when https://github.com/discordjs/discord.js/pull/7497 is merged + jsonTransformer }); patch(this); @@ -542,3 +545,9 @@ enum GatewayIntentBits { DirectMessageTyping = 16384, GuildScheduledEvents = 65536 } + +function jsonTransformer(obj: any): any { + if (typeof obj !== 'object' || !obj) return obj; + if (Array.isArray(obj)) return obj.map(jsonTransformer); + return Object.fromEntries(Object.entries(obj).map(([key, value]) => [snakeCase(key), jsonTransformer(value)])); +} diff --git a/src/lib/extensions/discord-akairo/BushClientUtil.ts b/src/lib/extensions/discord-akairo/BushClientUtil.ts index bf4dfaf..ecfa360 100644 --- a/src/lib/extensions/discord-akairo/BushClientUtil.ts +++ b/src/lib/extensions/discord-akairo/BushClientUtil.ts @@ -1,5 +1,6 @@ import { Arg, + BaseBushArgumentType, BushConstants, Global, Shared, @@ -21,7 +22,7 @@ import assert from 'assert'; import { exec } from 'child_process'; import deepLock from 'deep-lock'; import { ClientUtil, Util as AkairoUtil } from 'discord-akairo'; -import type { APIMessage } from 'discord-api-types/v9'; +import { APIMessage, OAuth2Scopes } from 'discord-api-types/v9'; import { Constants as DiscordConstants, GuildMember, @@ -266,31 +267,18 @@ export class BushClientUtil extends ClientUtil { * @returns The default options combined with the specified options. */ #getDefaultInspectOptions(options?: BushInspectOptions): BushInspectOptions { - const { - showHidden = false, - depth = 2, - colors = false, - customInspect = true, - showProxy = false, - maxArrayLength = Infinity, - maxStringLength = Infinity, - breakLength = 80, - compact = 3, - sorted = false, - getters = true - } = options ?? {}; return { - showHidden, - depth, - colors, - customInspect, - showProxy, - maxArrayLength, - maxStringLength, - breakLength, - compact, - sorted, - getters + showHidden: options?.showHidden ?? false, + depth: options?.depth ?? 2, + colors: options?.colors ?? false, + customInspect: options?.customInspect ?? true, + showProxy: options?.showProxy ?? false, + maxArrayLength: options?.maxArrayLength ?? Infinity, + maxStringLength: options?.maxStringLength ?? Infinity, + breakLength: options?.breakLength ?? 80, + compact: options?.compact ?? 3, + sorted: options?.sorted ?? false, + getters: options?.getters ?? true }; } @@ -556,7 +544,7 @@ export class BushClientUtil extends ClientUtil { * @returns The {@link ParsedDuration}. */ public parseDuration(content: string, remove = true): ParsedDuration { - if (!content) return { duration: 0, contentWithoutTime: null }; + if (!content) return { duration: 0, content: null }; // eslint-disable-next-line prefer-const let duration: number | null = null; @@ -574,7 +562,7 @@ export class BushClientUtil extends ClientUtil { } // remove the space added earlier if (contentWithoutTime.startsWith(' ')) contentWithoutTime.replace(' ', ''); - return { duration, contentWithoutTime }; + return { duration, content: contentWithoutTime }; } /** @@ -716,7 +704,7 @@ export class BushClientUtil extends ClientUtil { .catch(() => undefined)) as { pronouns: PronounCode } | undefined; if (!apiRes) return undefined; - if (!apiRes.pronouns) throw new Error('apiRes.pronouns is undefined'); + assert(apiRes.pronouns); return client.constants.pronounMapping[apiRes.pronouns!]!; } @@ -911,10 +899,10 @@ export class BushClientUtil extends ClientUtil { * The link to invite the bot with all permissions. */ public get invite() { - return `https://discord.com/api/oauth2/authorize?client_id=${Buffer.from( - client.token!.split('.')[0], - 'base64' - ).toString()}&permissions=${PermissionsBitField.All}&scope=bot%20applications.commands`; + return client.generateInvite({ + permissions: PermissionsBitField.All, + scopes: [OAuth2Scopes.Bot, OAuth2Scopes.ApplicationsCommands] + }); } public assertAll(...args: any[]): void { @@ -923,6 +911,23 @@ export class BushClientUtil extends ClientUtil { } } + public async castDurationContent( + arg: string | ParsedDuration | null, + message: BushMessage | BushSlashMessage + ): Promise<ParsedDurationRes> { + const res = typeof arg === 'string' ? await util.arg.cast('contentWithDuration', message, arg) : arg; + + return { duration: res?.duration ?? 0, content: res?.content ?? '' }; + } + + public async cast<T extends keyof BaseBushArgumentType>( + type: T, + arg: BaseBushArgumentType[T] | string, + message: BushMessage | BushSlashMessage + ) { + return typeof arg === 'string' ? await util.arg.cast(type, message, arg) : arg; + } + /** * A wrapper for the Argument class that adds custom typings. */ @@ -989,5 +994,10 @@ export interface HasteResults { export interface ParsedDuration { duration: number | null; - contentWithoutTime: string | null; + content: string | null; +} + +export interface ParsedDurationRes { + duration: number; + content: string; } diff --git a/src/lib/extensions/discord.js/BushCategoryChannel.ts b/src/lib/extensions/discord.js/BushCategoryChannel.ts index ac82bf0..3868b54 100644 --- a/src/lib/extensions/discord.js/BushCategoryChannel.ts +++ b/src/lib/extensions/discord.js/BushCategoryChannel.ts @@ -2,7 +2,6 @@ import { BushDMChannel, BushGuildBasedChannel, BushNewsChannel, - BushNonThreadGuildBasedChannel, BushStageChannel, BushStoreChannel, BushTextBasedChannel, @@ -10,11 +9,11 @@ import { BushThreadChannel, BushVoiceBasedChannel, BushVoiceChannel, + type BushCategoryChannelChildManager, type BushClient, - type BushGuild, - type BushGuildMember + type BushGuild } from '#lib'; -import { CategoryChannel, type Collection, type Snowflake } from 'discord.js'; +import { CategoryChannel } from 'discord.js'; import { type RawGuildChannelData } from 'discord.js/typings/rawDataTypes'; /** @@ -22,10 +21,8 @@ import { type RawGuildChannelData } from 'discord.js/typings/rawDataTypes'; */ export class BushCategoryChannel extends CategoryChannel { public declare readonly client: BushClient; - public declare readonly children: Collection<Snowflake, Exclude<BushNonThreadGuildBasedChannel, BushCategoryChannel>>; + public declare readonly children: BushCategoryChannelChildManager; public declare guild: BushGuild; - public declare readonly members: Collection<Snowflake, BushGuildMember>; - public declare readonly parent: CategoryChannel | null; public constructor(guild: BushGuild, data?: RawGuildChannelData, client?: BushClient, immediatePatch?: boolean) { super(guild, data, client, immediatePatch); diff --git a/src/lib/extensions/discord.js/BushCategoryChannelChildManager.ts b/src/lib/extensions/discord.js/BushCategoryChannelChildManager.ts new file mode 100644 index 0000000..b9a7ac7 --- /dev/null +++ b/src/lib/extensions/discord.js/BushCategoryChannelChildManager.ts @@ -0,0 +1,52 @@ +/* eslint-disable deprecation/deprecation */ +import type { + BushCategoryChannel, + BushGuild, + BushGuildChannelResolvable, + BushMappedChannelCategoryTypes, + BushNonCategoryGuildBasedChannel, + BushStoreChannel, + BushTextChannel +} from '#lib'; +import type { CategoryChannelType, CategoryCreateChannelOptions, ChannelType, DataManager, Snowflake } from 'discord.js'; + +export declare class BushCategoryChannelChildManager extends DataManager< + Snowflake, + BushNonCategoryGuildBasedChannel, + BushGuildChannelResolvable +> { + private constructor(channel: BushCategoryChannel); + + /** + * The category channel this manager belongs to + */ + public channel: BushCategoryChannel; + + /** + * The guild this manager belongs to + */ + public readonly guild: BushGuild; + + /** + * Creates a new channel within this category. + * <info>You cannot create a channel of type {@link ChannelType.GuildCategory} inside a CategoryChannel.</info> + * @param name The name of the new channel + * @param options Options for creating the new channel + */ + public create<T extends Exclude<CategoryChannelType, ChannelType.GuildStore>>( + name: string, + options: CategoryCreateChannelOptions & { type: T } + ): Promise<BushMappedChannelCategoryTypes[T]>; + /** + * Creates a new channel within this category. + * <info>You cannot create a channel of type {@link ChannelType.GuildCategory} inside a CategoryChannel.</info> + * @param name The name of the new channel + * @param options Options for creating the new channel + * @deprecated See [Self-serve Game Selling Deprecation](https://support-dev.discord.com/hc/en-us/articles/4414590563479) for more information + */ + public create( + name: string, + options: CategoryCreateChannelOptions & { type: ChannelType.GuildStore } + ): Promise<BushStoreChannel>; + public create(name: string, options?: CategoryCreateChannelOptions): Promise<BushTextChannel>; +} diff --git a/src/lib/extensions/discord.js/other.ts b/src/lib/extensions/discord.js/other.ts index 784442d..e4bc10b 100644 --- a/src/lib/extensions/discord.js/other.ts +++ b/src/lib/extensions/discord.js/other.ts @@ -161,3 +161,5 @@ export enum BushInteractionType { MessageComponent = 3, ApplicationCommandAutocomplete = 4 } + +export type BushNonCategoryGuildBasedChannel = Exclude<BushGuildBasedChannel, BushCategoryChannel>; diff --git a/src/lib/index.ts b/src/lib/index.ts index 7a9ab5f..45d76b7 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -28,6 +28,7 @@ export type { BushBaseGuildEmojiManager } from './extensions/discord.js/BushBase export type { BushBaseGuildVoiceChannel } from './extensions/discord.js/BushBaseGuildVoiceChannel.js'; export * from './extensions/discord.js/BushButtonInteraction.js'; export * from './extensions/discord.js/BushCategoryChannel.js'; +export type { BushCategoryChannelChildManager } from './extensions/discord.js/BushCategoryChannelChildManager.js'; export type { BushChannel } from './extensions/discord.js/BushChannel.js'; export type { BushChannelManager } from './extensions/discord.js/BushChannelManager.js'; export * from './extensions/discord.js/BushChatInputCommandInteraction.js'; diff --git a/src/lib/utils/CanvasProgressBar.ts b/src/lib/utils/CanvasProgressBar.ts index 1ba0e8b..21c4e22 100644 --- a/src/lib/utils/CanvasProgressBar.ts +++ b/src/lib/utils/CanvasProgressBar.ts @@ -1,3 +1,5 @@ +import { CanvasRenderingContext2D } from 'canvas'; + // I just copy pasted this code from stackoverflow don't yell at me if there is issues for it export class CanvasProgressBar { private readonly x: number; diff --git a/src/listeners/ws/INTERACTION_CREATE.ts b/src/listeners/ws/INTERACTION_CREATE.ts index f44c06c..25d9cfe 100644 --- a/src/listeners/ws/INTERACTION_CREATE.ts +++ b/src/listeners/ws/INTERACTION_CREATE.ts @@ -35,7 +35,7 @@ export default class WsInteractionCreateListener extends BushListener { } public override async exec(interaction: APIInteraction) { - console.dir(interaction); + // console.dir(interaction); const respond = ( options: |