diff options
27 files changed, 723 insertions, 439 deletions
diff --git a/package.json b/package.json index d7fa089..8bea052 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "prettier": "^2.3.2", "rimraf": "^3.0.2", "sequelize": "^6.5.0", + "simplify-number": "^1.0.0", "source-map-support": "^0.5.19", "tinycolor2": "^1.4.2", "typescript": "^4.4.2", diff --git a/src/arguments/roleWithDuation.ts b/src/arguments/roleWithDuation.ts new file mode 100644 index 0000000..423e7df --- /dev/null +++ b/src/arguments/roleWithDuation.ts @@ -0,0 +1,13 @@ +import { BushArgumentTypeCaster } from '@lib'; + +export const roleWithDurationTypeCaster: BushArgumentTypeCaster = async ( + message, + phrase +): Promise<{ duration: number; role: string | null } | null> => { + const { duration, contentWithoutTime } = client.util.parseDuration(phrase); + if (contentWithoutTime === null || contentWithoutTime === undefined) return null; + const role = await util.arg.cast('role', client.commandHandler.resolver, message, contentWithoutTime); + console.debug(['role'], [role], [contentWithoutTime]); + if (!role) return null; + return { duration, role }; +}; diff --git a/src/commands/config/blacklist.ts b/src/commands/config/blacklist.ts index 57c3015..ff34567 100644 --- a/src/commands/config/blacklist.ts +++ b/src/commands/config/blacklist.ts @@ -68,8 +68,8 @@ export default class BlacklistCommand extends BushCommand { const global = args.global && message.author.isOwner(); const target = typeof args.target === 'string' - ? (await Argument.cast('channel', client.commandHandler.resolver, message as BushMessage, args.target)) ?? - (await Argument.cast('user', client.commandHandler.resolver, message as BushMessage, args.target)) + ? (await util.arg.cast('channel', client.commandHandler.resolver, message as BushMessage, args.target)) ?? + (await util.arg.cast('user', client.commandHandler.resolver, message as BushMessage, args.target)) : args.target; if (!target) return await message.util.reply(`${util.emojis.error} Choose a valid channel or user.`); const targetID = target.id; diff --git a/src/commands/config/config.ts b/src/commands/config/config.ts new file mode 100644 index 0000000..8362144 --- /dev/null +++ b/src/commands/config/config.ts @@ -0,0 +1,351 @@ +import { BushCommand, BushMessage, BushSlashMessage, GuildSettings, guildSettingsObj, settingsArr } from '@lib'; +import { ArgumentOptions, Flag } from 'discord-akairo'; +import { + Channel, + Formatters, + Message, + MessageActionRow, + MessageButton, + MessageComponentInteraction, + MessageEmbed, + MessageOptions, + MessageSelectMenu, + Role +} from 'discord.js'; +import _ from 'lodash'; + +export default class SettingsCommand extends BushCommand { + public constructor() { + super('config', { + aliases: ['config', 'settings', 'setting', 'configure'], + category: 'config', + description: { + content: 'Configure server settings.', + usage: `settings (${settingsArr.map((s) => `\`${s}\``).join(', ')}) (${['view', 'set', 'add', 'remove'].map( + (s) => `\`${s}\`` + )})`, + examples: ['settings', 'config prefix set -'] + }, + slash: true, + slashOptions: settingsArr.map((setting) => { + return { + name: _.snakeCase(setting), + description: `Manage the server's ${guildSettingsObj[setting].name.toLowerCase()}`, + type: 'SUB_COMMAND_GROUP', + options: guildSettingsObj[setting].type.includes('-array') + ? [ + { + name: 'view', + description: `View the server's ${guildSettingsObj[setting].name.toLowerCase()}.`, + type: 'SUB_COMMAND' + }, + { + name: 'add', + description: `Add a value to the server's ${guildSettingsObj[setting].name.toLowerCase()}.`, + type: 'SUB_COMMAND', + options: [ + { + name: 'value', + description: `What would you like to add to the server's ${guildSettingsObj[ + setting + ].name.toLowerCase()}?'`, + type: guildSettingsObj[setting].type.replace('-array', '').toUpperCase() as 'ROLE' | 'STRING' | 'CHANNEL', + required: true + } + ] + }, + { + name: 'remove', + description: `Remove a value from the server's ${guildSettingsObj[setting].name.toLowerCase()}.`, + type: 'SUB_COMMAND', + options: [ + { + name: 'value', + description: `What would you like to remove from the server's ${guildSettingsObj[ + setting + ].name.toLowerCase()}?'`, + type: guildSettingsObj[setting].type.replace('-array', '').toUpperCase() as 'ROLE' | 'STRING' | 'CHANNEL', + required: true + } + ] + } + ] + : [ + { + name: 'view', + description: `View the server's ${guildSettingsObj[setting].name.toLowerCase()}.`, + type: 'SUB_COMMAND' + }, + { + name: 'set', + description: `Set the server's ${guildSettingsObj[setting].name.toLowerCase()}.`, + type: 'SUB_COMMAND', + options: [ + { + name: 'value', + description: `What would you like to set the server's ${guildSettingsObj[ + setting + ].name.toLowerCase()} to?'`, + type: guildSettingsObj[setting].type.toUpperCase() as 'ROLE' | 'STRING' | 'CHANNEL', + required: true + } + ] + } + ] + }; + }), + slashGuilds: ['516977525906341928', '812400566235430912'], + channel: 'guild', + clientPermissions: ['SEND_MESSAGES'], + userPermissions: ['SEND_MESSAGES', 'MANAGE_GUILD'], + ownerOnly: true + }); + } + + // I make very readable code :) + *args(message: BushMessage): IterableIterator<ArgumentOptions | Flag> { + const optional = message.util.parsed!.alias === 'settings'; + const setting = yield { + id: 'setting', + type: settingsArr, + prompt: { + start: `What setting would you like to see or change? You can choose one of the following: ${settingsArr + .map((s) => `\`${s}\``) + .join(', ')}`, + retry: `{error} Choose one of the following settings: ${settingsArr.map((s) => `\`${s}\``).join(', ')}`, + optional + } + }; + + const actionType = setting + ? guildSettingsObj[setting as unknown as GuildSettings]?.type.includes('-array') + ? ['view', 'add', 'remove'] + : ['view', 'set'] + : undefined; + + const action = setting + ? yield { + id: 'action', + type: actionType, + prompt: { + start: `Would you like to ${util.oxford( + actionType!.map((a) => `\`${a}\``), + 'or' + )} the \`${setting}\` setting?`, + retry: `{error} Choose one of the following actions to perform on the ${setting} setting: ${util.oxford( + actionType!.map((a) => `\`${a}\``), + 'or' + )}`, + optional + } + } + : undefined; + + const valueType = + setting && action && action !== 'view' + ? (guildSettingsObj[setting as unknown as GuildSettings].type.replace('-array', '') as 'string' | 'channel' | 'role') + : undefined; + const grammar = + setting && action && action !== 'view' + ? (action as unknown as 'add' | 'remove' | 'set') === 'add' + ? `to the ${setting} setting` + : (action as unknown as 'remove' | 'set') === 'remove' + ? `from the ${setting} setting` + : `the ${setting} setting to` + : undefined; + + const value = + setting && action && action !== 'view' + ? yield { + id: 'value', + type: valueType, + match: 'restContent', + prompt: { + start: `What would you like to ${action} ${grammar}?`, + retry: `{error} You must choose a ${valueType === 'string' ? 'value' : valueType} to ${action} ${grammar}.`, + optional + } + } + : undefined; + + return { setting, action, value }; + } + + public override async exec( + message: BushMessage | BushSlashMessage, + args: { + setting?: GuildSettings; + subcommandGroup?: GuildSettings; + action?: 'view' | 'add' | 'remove' | 'set'; + subcommand?: 'view' | 'add' | 'remove' | 'set'; + value: string | Channel | Role; + } + ): Promise<unknown> { + if (!message.guild) return await message.util.reply(`${util.emojis.error} This command can only be used in servers.`); + if (!message.member?.permissions.has('MANAGE_GUILD')) + return await message.util.reply( + `${util.emojis.error} You must have the **MANAGE_GUILD** permissions 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!; + const value = args.value; + + let msg; + + if (!setting || action === 'view') { + const messageOptions = await this.generateMessageOptions(message, setting ?? undefined); + msg = (await message.util.reply(messageOptions)) as Message; + } else { + const parseVal = (val: string | Channel | Role) => { + if (val instanceof Channel || val instanceof Role) { + return val.id; + } + return val; + }; + + if (!value) + return await message.util.reply( + `${util.emojis.error} You must choose a value to ${action} ${ + (action as unknown as 'add' | 'remove' | 'set') === 'add' + ? `to the ${setting} setting` + : (action as unknown as 'remove' | 'set') === 'remove' + ? `from the ${setting} setting` + : `the ${setting} setting to` + }` + ); + switch (action) { + case 'add': + case 'remove': { + const existing = (await message.guild.getSetting(setting)) as string[]; + const updated = util.addOrRemoveFromArray('add', existing, parseVal(value)); + await message.guild.setSetting(setting, updated); + const messageOptions = await this.generateMessageOptions(message, setting); + msg = (await message.util.reply(messageOptions)) as Message; + break; + } + case 'set': { + await message.guild.setSetting(setting, parseVal(value)); + const messageOptions = await this.generateMessageOptions(message, setting); + msg = (await message.util.reply(messageOptions)) as Message; + break; + } + } + } + const collector = msg.createMessageComponentCollector({ + channel: message.channel ?? undefined, + guild: message.guild, + message: message as Message, + time: 300_000 + }); + + 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'); + switch (interaction.customId) { + case 'command_settingsSel': { + if (!interaction.isSelectMenu()) return; + + return interaction.update( + await this.generateMessageOptions(message, interaction.values[0] as keyof typeof guildSettingsObj) + ); + } + case 'command_settingsBack': { + if (!interaction.isButton()) return; + + return interaction.update(await this.generateMessageOptions(message)); + } + } + } else { + return await interaction?.deferUpdate().catch(() => undefined); + } + }); + } + + public async generateMessageOptions( + message: BushMessage | BushSlashMessage, + setting?: undefined | keyof typeof guildSettingsObj + ): Promise<MessageOptions> { + if (!message.guild) throw new Error('message.guild is null'); + const settingsEmbed = new MessageEmbed().setColor(util.colors.default); + if (!setting) { + settingsEmbed.setTitle(`${message.guild!.name}'s Settings`); + const desc = settingsArr.map((s) => `:wrench: **${guildSettingsObj[s].name}**`).join('\n'); + settingsEmbed.setDescription(desc); + + const selMenu = new MessageActionRow().addComponents( + new MessageSelectMenu() + .addOptions( + ...settingsArr.map((s) => ({ + label: guildSettingsObj[s].name, + value: s, + description: guildSettingsObj[s].description + })) + ) + .setPlaceholder('Select A Setting to View') + .setMaxValues(1) + .setMinValues(1) + .setCustomId('command_settingsSel') + ); + return { embeds: [settingsEmbed], components: [selMenu] }; + } else { + settingsEmbed.setTitle(guildSettingsObj[setting].name); + const generateCurrentValue = async ( + type: 'string' | 'channel' | 'channel-array' | 'role' | 'role-array' + ): Promise<string> => { + const feat = await message.guild!.getSetting(setting); + + switch (type.replace('-array', '') as 'string' | 'channel' | 'role') { + case 'string': { + return Array.isArray(feat) + ? feat.length + ? feat.map((feat) => util.discord.escapeInlineCode(util.inspectAndRedact(feat))).join('\n') + : '[Empty Array]' + : feat !== null + ? util.discord.escapeInlineCode(util.inspectAndRedact(feat)) + : '[No Value Set]'; + } + case 'channel': { + return Array.isArray(feat) + ? feat.length + ? feat.map((feat) => `<#${feat}>`).join('\n') + : '[Empty Array]' + : `<#${feat}>`; + } + case 'role': { + return Array.isArray(feat) + ? feat.length + ? feat.map((feat) => `<@&${feat}>`).join('\n') + : '[Empty Array]' + : `<@&${feat}>`; + } + } + }; + + const components = new MessageActionRow().addComponents( + new MessageButton().setStyle('PRIMARY').setCustomId('command_settingsBack').setLabel('Back') + ); + settingsEmbed.setDescription( + `${Formatters.italic(guildSettingsObj[setting].description)}\n\n**Type**: ${guildSettingsObj[setting].type}` + ); + + settingsEmbed.setFooter( + `Run "${ + message.util.isSlash + ? '/' + : client.config.isDevelopment + ? 'dev ' + : message.util.parsed?.prefix ?? client.config.prefix + }${message.util.parsed?.alias ?? 'config'} ${setting} ${ + guildSettingsObj[setting].type.includes('-array') ? 'add/remove' : 'set' + } <value>" to set this setting.` + ); + settingsEmbed.addField( + 'value', + (await generateCurrentValue( + guildSettingsObj[setting].type as 'string' | 'channel' | 'channel-array' | 'role' | 'role-array' + )) || '[No Value Set]' + ); + return { embeds: [settingsEmbed], components: [components] }; + } + } +} diff --git a/src/commands/config/joinRoles.ts b/src/commands/config/joinRoles.ts index 9507d4b..0b9ac21 100644 --- a/src/commands/config/joinRoles.ts +++ b/src/commands/config/joinRoles.ts @@ -40,10 +40,8 @@ export default class JoinRolesCommand extends BushCommand { public override async exec(message: BushMessage | BushSlashMessage, { role }: { role: Role }): Promise<unknown> { const joinRoles = await message.guild!.getSetting('joinRoles'); const includes = joinRoles.includes(role.id); - client.console.debug(joinRoles); const newValue = util.addOrRemoveFromArray(includes ? 'remove' : 'add', joinRoles, role.id); await message.guild!.setSetting('joinRoles', newValue); - client.console.debug(joinRoles); return await message.util.reply({ content: `${util.emojis.success} Successfully ${includes ? 'removed' : 'added'} <@&${role.id}> ${ includes ? 'from' : 'to' diff --git a/src/commands/config/settings.ts b/src/commands/config/settings.ts deleted file mode 100644 index a8070e2..0000000 --- a/src/commands/config/settings.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { BushCommand, BushMessage, BushSlashMessage, guildSettingsObj, settingsArr } from '@lib'; -import { - Message, - MessageActionRow, - MessageButton, - MessageComponentInteraction, - MessageEmbed, - MessageOptions, - MessageSelectMenu -} from 'discord.js'; - -export default class SettingsCommand extends BushCommand { - public constructor() { - super('settings', { - aliases: ['settings', 'setting', 'configure', 'config'], - category: 'config', - description: { - content: 'Configure server options.', - usage: 'settings', - examples: ['settings'] - }, - slash: true, - slashOptions: settingsArr.map((setting) => { - return { - name: util.camelToSnakeCase(setting), - description: `Manage the server's ${guildSettingsObj[setting].name.toLowerCase()}`, - type: 'SUB_COMMAND_GROUP', - options: guildSettingsObj[setting].type.includes('-array') - ? [ - { - name: 'view', - description: `View the server's ${guildSettingsObj[setting].name.toLowerCase()}.`, - type: 'SUB_COMMAND' - }, - { - name: 'add', - description: `Add a value to the server's ${guildSettingsObj[setting].name.toLowerCase()}.`, - type: 'SUB_COMMAND', - options: [ - { - name: 'value', - description: `What would you like to add to the server's ${guildSettingsObj[ - setting - ].name.toLowerCase()}?'`, - type: guildSettingsObj[setting].type.replace('-array', '').toUpperCase() as 'ROLE' | 'STRING' | 'CHANNEL', - required: true - } - ] - }, - { - name: 'remove', - description: `Remove a value from the server's ${guildSettingsObj[setting].name.toLowerCase()}.`, - type: 'SUB_COMMAND', - options: [ - { - name: 'value', - description: `What would you like to remove from the server's ${guildSettingsObj[ - setting - ].name.toLowerCase()}?'`, - type: guildSettingsObj[setting].type.replace('-array', '').toUpperCase() as 'ROLE' | 'STRING' | 'CHANNEL', - required: true - } - ] - } - ] - : [ - { - name: 'view', - description: `View the server's ${guildSettingsObj[setting].name.toLowerCase()}.`, - type: 'SUB_COMMAND' - }, - { - name: 'set', - description: `Set the server's ${guildSettingsObj[setting].name.toLowerCase()}.`, - type: 'SUB_COMMAND', - options: [ - { - name: 'value', - description: `What would you like to set the server's ${guildSettingsObj[ - setting - ].name.toLowerCase()} to?'`, - type: guildSettingsObj[setting].type.toUpperCase() as 'ROLE' | 'STRING' | 'CHANNEL', - required: true - } - ] - } - ] - }; - }), - slashGuilds: ['516977525906341928', '812400566235430912'], - channel: 'guild', - clientPermissions: ['SEND_MESSAGES'], - userPermissions: ['SEND_MESSAGES', 'MANAGE_GUILD'], - ownerOnly: true - }); - } - - // *args(): any {} - - public override async exec(message: BushMessage | BushSlashMessage, args: unknown): Promise<unknown> { - client.console.debugRaw(message.interaction); - client.console.debugRaw(args); - if (!message.guild) return await message.util.reply(`${util.emojis.error} This command can only be used in servers.`); - const messageOptions = await this.generateMessageOptions(message); - const msg = (await message.util.reply(messageOptions)) as Message; - const collector = msg.createMessageComponentCollector({ - channel: message.channel ?? undefined, - guild: message.guild, - message: message as Message, - time: 300_000 - }); - - 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'); - switch (interaction.customId) { - case 'command_settingsSel': { - if (!interaction.isSelectMenu()) return; - - return interaction.update( - await this.generateMessageOptions(message, interaction.values[0] as keyof typeof guildSettingsObj) - ); - } - } - } else { - return await interaction?.deferUpdate().catch(() => undefined); - } - }); - } - - public async generateMessageOptions( - message: BushMessage | BushSlashMessage, - feature?: keyof typeof guildSettingsObj - ): Promise<MessageOptions> { - if (!message.guild) throw new Error('message.guild is null'); - const settingsEmbed = new MessageEmbed().setTitle(`${message.guild!.name}'s Settings`).setColor(util.colors.default); - if (!feature) { - const desc = settingsArr.map((s) => `**${guildSettingsObj[s].name}**`).join('\n'); - settingsEmbed.setDescription(desc); - - const selMenu = new MessageActionRow().addComponents( - new MessageSelectMenu() - .addOptions( - ...settingsArr.map((s) => ({ - label: guildSettingsObj[s].name, - value: s, - description: guildSettingsObj[s].description - })) - ) - .setPlaceholder('Select A Setting to View') - .setMaxValues(1) - .setMinValues(1) - .setCustomId('command_settingsSel') - ); - return { embeds: [settingsEmbed], components: [selMenu] }; - } else { - const generateCurrentValue = async ( - type: 'string' | 'channel' | 'channel-array' | 'role' | 'role-array' - ): Promise<string> => { - const feat = await message.guild!.getSetting(feature); - switch (type.replace('-array', '') as 'string' | 'channel' | 'role') { - case 'string': { - return Array.isArray(feat) - ? feat.map((feat) => util.discord.escapeInlineCode(util.inspectAndRedact(feat))).join('\n') - : util.discord.escapeInlineCode(util.inspectAndRedact(feat)); - } - case 'channel': { - return Array.isArray(feat) ? feat.map((feat) => `<#${feat}>`).join('\n') : `<#${feat}>`; - } - case 'role': { - return Array.isArray(feat) ? feat.map((feat) => `<@&${feat}>`).join('\n') : `<@&${feat}>`; - } - } - }; - const components = new MessageActionRow().addComponents( - new MessageButton().setStyle('PRIMARY').setCustomId('command_settingsBack').setLabel('Back') - ); - settingsEmbed.setDescription(guildSettingsObj[feature].description); - - settingsEmbed.setFooter( - `Run "${message.util.isSlash ? '/' : await message.guild.getSetting('prefix')}settings ${feature} ${ - guildSettingsObj[feature].type.includes('-array') ? 'add/remove' : 'set' - } <value>" to set this setting.` - ); - settingsEmbed.addField( - guildSettingsObj[feature].name, - await generateCurrentValue(feature as 'string' | 'channel' | 'channel-array' | 'role' | 'role-array') - ); - return { embeds: [settingsEmbed], components: [components] }; - } - } -} diff --git a/src/commands/dev/superUser.ts b/src/commands/dev/superUser.ts index 957e2b7..1b2fd7c 100644 --- a/src/commands/dev/superUser.ts +++ b/src/commands/dev/superUser.ts @@ -1,4 +1,5 @@ import { BushCommand, BushMessage, BushSlashMessage, Global } from '@lib'; +import { ArgumentOptions, Flag } from 'discord-akairo'; import { User } from 'discord.js'; export default class SuperUserCommand extends BushCommand { @@ -15,14 +16,14 @@ export default class SuperUserCommand extends BushCommand { ownerOnly: true }); } - *args(): unknown { + *args(): IterableIterator<ArgumentOptions | Flag> { const action = yield { id: 'action', type: ['add', 'remove'], prompt: { start: 'Would you like to `add` or `remove` a user from the superuser list?', retry: '{error} Choose if you would like to `add` or `remove` a user.', - required: true + optional: false } }; const user = yield { @@ -30,9 +31,9 @@ export default class SuperUserCommand extends BushCommand { type: 'user', match: 'restContent', prompt: { - start: `Who would you like to ${action || 'add/remove'} from the superuser list?`, - retry: `Choose a valid user to ${action || 'add/remove'} from the superuser list.`, - required: true + start: `Who would you like to ${action ?? 'add/remove'} from the superuser list?`, + retry: `Choose a valid user to ${action ?? 'add/remove'} from the superuser list.`, + optional: false } }; return { action, user }; diff --git a/src/commands/info/avatar.ts b/src/commands/info/avatar.ts index 33393b8..7654d2f 100644 --- a/src/commands/info/avatar.ts +++ b/src/commands/info/avatar.ts @@ -1,4 +1,4 @@ -import { CommandInteraction, MessageEmbed, User } from 'discord.js'; +import { MessageEmbed, User } from 'discord.js'; import { BushCommand, BushMessage, BushSlashMessage } from '../../lib'; export default class AvatarCommand extends BushCommand { @@ -36,9 +36,6 @@ export default class AvatarCommand extends BushCommand { } override async exec(message: BushMessage | BushSlashMessage, args: { user: User }): Promise<void> { - client.console.debugRaw(args); - client.console.debugRaw(message.interaction); - client.console.debugRaw((message.interaction as CommandInteraction).options.getUser('user')); const user = args.user ?? message.author; const embed = new MessageEmbed() diff --git a/src/commands/info/help.ts b/src/commands/info/help.ts index 97811da..ad4e00f 100644 --- a/src/commands/info/help.ts +++ b/src/commands/info/help.ts @@ -47,7 +47,11 @@ export default class HelpCommand extends BushCommand { message: BushMessage | BushSlashMessage, args: { command: BushCommand | string; showHidden?: boolean } ): Promise<unknown> { - const prefix = client.config.isDevelopment ? 'dev ' : message.util.parsed?.prefix ?? client.config.prefix; + const prefix = message.util.isSlash + ? '/' + : client.config.isDevelopment + ? 'dev ' + : message.util.parsed?.prefix ?? client.config.prefix; const row = new MessageActionRow(); if (!client.config.isDevelopment && !client.guilds.cache.some((guild) => guild.ownerId === message.author.id)) { diff --git a/src/commands/leveling/leaderboard.ts b/src/commands/leveling/leaderboard.ts new file mode 100644 index 0000000..b8838b7 --- /dev/null +++ b/src/commands/leveling/leaderboard.ts @@ -0,0 +1,52 @@ +import { BushCommand, BushMessage, BushSlashMessage, Level } from '@lib'; +import { MessageEmbed } from 'discord.js'; + +export default class LeaderboardCommand extends BushCommand { + public constructor() { + super('leaderboard', { + aliases: ['leaderboard', 'lb'], + category: 'leveling', + description: { + content: 'Allows you to see the users with the highest levels in the server.', + usage: 'leaderboard [page]', + examples: ['leaderboard 5'] + }, + args: [ + { + id: 'page', + type: 'integer', + prompt: { + start: 'What would you like to set your first argument to be?', + retry: '{error} Pick a valid argument.', + optional: true + } + } + ], + slash: true, + slashOptions: [ + { + name: 'page', + description: 'What would you like to set your first argument to be?', + type: 'INTEGER', + required: false + } + ], + channel: 'guild', + clientPermissions: ['SEND_MESSAGES'], + userPermissions: ['SEND_MESSAGES'] + }); + } + + public override async exec(message: BushMessage | BushSlashMessage, args: { page: number }): Promise<unknown> { + if (!message.guild) return await message.util.reply(`${util.emojis.error} This command can only be run in a server.`); + const ranks = (await Level.findAll({ where: { guild: message.guild.id } })).sort((a, b) => b.xp - a.xp); + const mapedRanks = ranks.map( + (val, index) => `\`${index + 1}\` <@${val.user}> - Level ${val.level} (${val.xp.toLocaleString()} xp)` + ); + const chunked = util.chunk(mapedRanks, 25); + const embeds = chunked.map((c) => + new MessageEmbed().setTitle(`${message.guild!.name}'s Leaderboard`).setDescription(c.join('\n')) + ); + return await util.buttonPaginate(message, embeds, null, true, args?.page ?? undefined); + } +} diff --git a/src/commands/leveling/level.ts b/src/commands/leveling/level.ts new file mode 100644 index 0000000..6640744 --- /dev/null +++ b/src/commands/leveling/level.ts @@ -0,0 +1,143 @@ +import { + AllowedMentions, + BushCommand, + BushGuild, + BushMessage, + BushSlashMessage, + BushUser, + CanvasProgressBar, + Level +} from '@lib'; +import canvas from 'canvas'; +import { MessageAttachment } from 'discord.js'; +import got from 'got/dist/source'; +import { join } from 'path'; +import SimplifyNumber from 'simplify-number'; + +export default class LevelCommand extends BushCommand { + public constructor() { + super('level', { + aliases: ['level', 'rank', 'lvl'], + category: 'leveling', + description: { + content: 'Shows the level of a user', + usage: 'level [user]', + examples: ['level', 'level @Tyman'] + }, + args: [ + { + id: 'user', + type: 'user', + prompt: { + start: 'What user would you like to see the level of?', + retry: '{error} Choose a valid user to see the level of.', + optional: true + } + } + ], + slashOptions: [ + { + name: 'user', + description: 'The user to get the level of', + type: 'USER', + required: false + } + ], + slash: true, + channel: 'guild' + }); + } + + private async getImage(user: BushUser, guild: BushGuild): Promise<Buffer> { + // I added comments because this code is impossible to read + const guildRows = await Level.findAll({ where: { guild: guild.id } }); + const rank = guildRows.sort((a, b) => b.xp - a.xp); + const userLevelRow = guildRows.find((a) => a.user === user.id); + if (!userLevelRow) throw new Error('User does not have a level'); + const userLevel = userLevelRow.level; + const currentLevelXP = Level.convertLevelToXp(userLevel); + const currentLevelXPProgress = userLevelRow.xp - currentLevelXP; + const xpForNextLevel = Level.convertLevelToXp(userLevelRow.level + 1) - currentLevelXP; + const white = '#FFFFFF', + gray = '#23272A', + highlight = user.hexAccentColor ?? '#5865F2'; + // Load roboto font because yes + canvas.registerFont(join(__dirname, '..', '..', '..', '..', 'lib', 'assets', 'Roboto-Regular.ttf'), { + family: 'Roboto' + }); + // Create image canvas + const image = canvas.createCanvas(800, 200), + ctx = image.getContext('2d'); + // Fill background + ctx.fillStyle = gray; + ctx.fillRect(0, 0, image.width, image.height); + // Draw avatar + const avatarBuffer = await got.get(user.displayAvatarURL({ format: 'png', size: 128 })).buffer(); + const avatarImage = new canvas.Image(); + avatarImage.src = avatarBuffer; + avatarImage.height = 128; + avatarImage.width = 128; + const imageTopCoord = image.height / 2 - avatarImage.height / 2; + ctx.drawImage(avatarImage, imageTopCoord, imageTopCoord); + // Write tag of user + ctx.font = '30px Roboto'; + ctx.fillStyle = white; + const measuredTag = ctx.measureText(user.tag); + ctx.fillText(user.tag, avatarImage.width + 70, 60); + // Draw line under tag + ctx.fillStyle = highlight; + ctx.fillRect(avatarImage.width + 70, 65 + measuredTag.actualBoundingBoxDescent, measuredTag.width, 3); + // Draw leveling bar + const fullProgressBar = new CanvasProgressBar( + ctx, + { + x: avatarImage.width + 70, + y: avatarImage.height - 0, + height: 30, + width: 550 + }, + white, + 1 + ); + fullProgressBar.draw(); + const progressBar = new CanvasProgressBar( + ctx, + { + x: avatarImage.width + 70, + y: avatarImage.height - 0, + height: 30, + width: 550 + }, + highlight, + currentLevelXPProgress / xpForNextLevel + ); + progressBar.draw(); + // Draw level data text + ctx.fillStyle = white; + ctx.fillText( + `Level: ${userLevel} XP: ${SimplifyNumber(currentLevelXPProgress)}/${SimplifyNumber( + xpForNextLevel + )} Rank: ${SimplifyNumber(rank.indexOf(rank.find((x) => x.user === user.id)!) + 1)}`, + avatarImage.width + 70, + avatarImage.height - 20 + ); + // Return image in buffer form + return image.toBuffer(); + } + + public override async exec(message: BushMessage | BushSlashMessage, args: { user?: BushUser }): Promise<unknown> { + const user = args.user ?? message.author; + try { + return await message.util.reply({ + 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') { + return await message.util.reply({ + content: `${util.emojis.error} ${user} does not have a level.`, + allowedMentions: AllowedMentions.none() + }); + } else throw e; + } + } +} diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 7f1a67c..c33b39a 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -1,5 +1,4 @@ import { AllowedMentions, BushCommand, BushGuildMember, BushMessage, BushSlashMessage } from '@lib'; -import { Argument } from 'discord-akairo'; import { User } from 'discord.js'; export default class BanCommand extends BushCommand { @@ -110,7 +109,7 @@ export default class BanCommand extends BushCommand { if (reason) { time = typeof reason === 'string' - ? await Argument.cast('duration', client.commandHandler.resolver, message as BushMessage, reason) + ? await util.arg.cast('duration', client.commandHandler.resolver, message as BushMessage, reason) : reason.duration; } const parsedReason = reason?.contentWithoutTime ?? ''; diff --git a/src/commands/moderation/modlog.ts b/src/commands/moderation/modlog.ts index 04264b8..ef0a56e 100644 --- a/src/commands/moderation/modlog.ts +++ b/src/commands/moderation/modlog.ts @@ -42,8 +42,7 @@ export default class ModlogCommand extends BushCommand { `**Moderator**: <@!${log.moderator}> (${log.moderator})` ]; if (log.duration) modLog.push(`**Duration**: ${util.humanizeDuration(log.duration)}`); - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - modLog.push(`**Reason**: ${log.reason || 'No Reason Specified.'}`); + modLog.push(`**Reason**: ${log.reason ?? 'No Reason Specified.'}`); if (log.evidence) modLog.push(`**Evidence:** ${log.evidence}`); return modLog.join(`\n`); } diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index 7b8689a..915302e 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -1,5 +1,4 @@ import { AllowedMentions, BushCommand, BushMessage, BushSlashMessage, BushUser } from '@lib'; -import { Argument } from 'discord-akairo'; export default class MuteCommand extends BushCommand { public constructor() { @@ -77,7 +76,7 @@ export default class MuteCommand extends BushCommand { if (reason) { time = typeof reason === 'string' - ? await Argument.cast('duration', client.commandHandler.resolver, message as BushMessage, reason) + ? await util.arg.cast('duration', client.commandHandler.resolver, message as BushMessage, reason) : reason.duration; } const parsedReason = reason?.contentWithoutTime ?? ''; diff --git a/src/commands/moderation/role.ts b/src/commands/moderation/role.ts index 4575a11..ddaefaa 100644 --- a/src/commands/moderation/role.ts +++ b/src/commands/moderation/role.ts @@ -1,51 +1,16 @@ -/* eslint-disable @typescript-eslint/no-empty-function */ import { AllowedMentions, BushCommand, BushGuildMember, BushMessage, BushRole, BushSlashMessage } from '@lib'; +import { ArgumentOptions, Flag } from 'discord-akairo'; export default class RoleCommand extends BushCommand { public constructor() { super('role', { - aliases: ['role'], + aliases: ['role', 'rr', 'ar', 'ra'], category: 'moderation', description: { content: "Manages users' roles.", usage: 'role <add|remove> <user> <role> [duration]', examples: ['role add spammer nogiveaways 7days'] }, - args: [ - { - id: 'action', - customType: [['add'], ['remove']], - prompt: { - start: 'Would you like to `add` or `remove` a role?', - retry: '{error} Choose whether you would you like to `add` or `remove` a role.' - } - }, - { - id: 'user', - type: 'member', - prompt: { - start: `What user do you want to add/remove the role to/from?`, - retry: `{error} Choose a valid user to add/remove the role to/from.` - } - }, - { - id: 'role', - type: 'role', - prompt: { - start: `What role do you want to add/remove to/from the user?`, - retry: `{error} Choose a valid role to add/remove.` - } - }, - { - id: 'duration', - type: 'duration', - prompt: { - start: 'How long would you like to role to last?', - retry: '{error} Choose a valid duration.', - optional: true - } - } - ], slash: true, slashOptions: [ { @@ -90,9 +55,56 @@ export default class RoleCommand extends BushCommand { }); } + *args(message: BushMessage): IterableIterator<ArgumentOptions | Flag> { + const action = ['rr'].includes(message.util.parsed?.alias ?? '') + ? 'remove' + : ['ar', 'ra'].includes(message.util.parsed?.alias ?? '') + ? 'add' + : yield { + id: 'action', + type: [['add'], ['remove']], + prompt: { + start: 'Would you like to `add` or `remove` a role?', + retry: (...arg) => { + console.debug(...arg); + return '{error} Choose whether you would you like to `add` or `remove` a role.'; + } + } + }; + console.debug(action); + const user = yield { + id: 'user', + type: 'member', + prompt: { + start: `What user do you want to ${action} the role ${action === 'add' ? 'to' : 'from'}?`, + retry: (...arg) => { + console.debug(...arg); + return `{error} Choose a valid user to ${action} the role ${action === 'add' ? 'to' : 'from'}.`; + } + } + }; + console.debug(user); + const _role = yield { + id: 'role', + type: `${action === 'add' ? 'roleWithDuration' : 'role'}`, + match: 'rest', + prompt: { + start: `What role do you want to ${action} ${action === 'add' ? 'to' : 'from'} the user${ + action === 'add' ? ', and for how long' : '' + }?`, + retry: (...arg) => { + console.debug(...arg); + return `{error} Choose a valid role to ${action}.`; + } + } + }; + console.debug(_role); + return { action, user, role: (_role as any).role ?? _role, duration: (_role as any).duration }; + } + public override async exec( message: BushMessage | BushSlashMessage, - { action, user, role, duration }: { action: 'add' | 'remove'; user: BushGuildMember; role: BushRole; duration: number } + { action, user, role, duration }: { action: 'add' | 'remove'; user: BushGuildMember; role: BushRole; duration?: number } ): Promise<unknown> { if (!message.member!.permissions.has('MANAGE_ROLES')) { const mappings = client.consts.mappings; @@ -131,6 +143,8 @@ export default class RoleCommand extends BushCommand { const responseMessage = () => { switch (responseCode) { case 'user hierarchy': + client.console.debug(role.position); + client.console.debug(user.roles.highest.position); return `${util.emojis.error} <@&${role.id}> is higher or equal to your highest role.`; case 'role managed': return `${util.emojis.error} <@&${role.id}> is managed by an integration and cannot be managed.`; diff --git a/src/commands/moderation/slowmode.ts b/src/commands/moderation/slowmode.ts index 1d47616..04fe3e4 100644 --- a/src/commands/moderation/slowmode.ts +++ b/src/commands/moderation/slowmode.ts @@ -66,12 +66,11 @@ export default class SlowModeCommand extends BushCommand { if (length) { length = typeof length === 'string' && !['off', 'none', 'disable'].includes(length) - ? await Argument.cast('duration', client.commandHandler.resolver, message as BushMessage, length) + ? await util.arg.cast('duration', client.commandHandler.resolver, message as BushMessage, length) : length; } - // @ts-expect-error: stop being dumb smh - const length2: number = ['off', 'none', 'disable'].includes(length) ? 0 : length; + const length2: number = ['off', 'none', 'disable'].includes(length as string) ? 0 : (length as number); const setSlowmode = await (channel as ThreadChannel | TextChannel) .setRateLimitPerUser(length2 / 1000, `Changed by ${message.author.tag} (${message.author.id}).`) diff --git a/src/commands/moulberry-bush/level.ts b/src/commands/moulberry-bush/level.ts deleted file mode 100644 index 02d66be..0000000 --- a/src/commands/moulberry-bush/level.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { BushCommand, BushGuild, BushMessage, BushSlashMessage, BushUser, Level } from '@lib'; -/* -import canvas from 'canvas'; -import { MessageAttachment } from 'discord.js'; -import { join } from 'path'; -import got from 'got/dist/source'; -import { CanvasProgressBar } from '@lib'; -*/ - -export default class LevelCommand extends BushCommand { - public constructor() { - super('level', { - aliases: ['level', 'rank'], - category: "Moulberry's Bush", - description: { - content: 'Shows the level of a user', - usage: 'level [user]', - examples: ['level', 'level @Tyman'] - }, - args: [ - { - id: 'user', - type: 'user', - prompt: { - start: 'What user would you like to see the level of?', - retry: '{error} Choose a valid user to see the level of.', - optional: true - } - } - ], - slashOptions: [ - { - name: 'user', - description: 'The user to get the level of', - type: 'USER', - required: false - } - ], - slash: true, - channel: 'guild' - }); - } - - /* private simplifyXP(xp: number): string { - - } - - private async getImage(user: User): Promise<Buffer> { - // I added comments because this code is impossible to read - const [userLevelRow] = await Level.findOrBuild({ - where: { - id: user.id - }, - defaults: { - id: user.id - } - }); - const userLevel = userLevelRow.level - const currentLevelXP = Level.convertLevelToXp(userLevel); - const currentLevelXPProgress = userLevelRow.xp - currentLevelXP; - const xpForNextLevel = - Level.convertLevelToXp(userLevelRow.level + 1) - currentLevelXP; - // Load roboto font because yes - canvas.registerFont( - join(__dirname, '..', '..', '..', 'Roboto-Regular.ttf'), - { - family: 'Roboto' - } - ); - // Create image canvas - const image = canvas.createCanvas(800, 200), - ctx = image.getContext('2d'); - // Fill background - ctx.fillStyle = '#00c7eb'; - ctx.fillRect(0, 0, image.width, image.height); - // Draw avatar - const avatarBuffer = await got - .get(user.displayAvatarURL({ format: 'png', size: 128 })) - .buffer(); - const avatarImage = new canvas.Image(); - avatarImage.src = avatarBuffer; - avatarImage.height = 128 - avatarImage.width = 128 - const imageTopCoord = (image.height / 2) - (avatarImage.height / 2) - ctx.drawImage(avatarImage, imageTopCoord, imageTopCoord); - // Write tag of user - ctx.font = '30px Roboto'; - ctx.fillStyle = 'black'; - const measuredTag = ctx.measureText(user.tag); - ctx.fillText(user.tag, avatarImage.width + 70, 60); - // Draw line under tag - ctx.fillStyle = 'yellow'; - ctx.fillRect( - avatarImage.width + 70, - 65 + measuredTag.actualBoundingBoxDescent, - measuredTag.width, - 3 - ); - // Draw leveling bar - const fullProgressBar = new CanvasProgressBar( - ctx, - { - x: avatarImage.width + 70, - y: avatarImage.height - 10, - height: 30, - width: 550 - }, - '#6e6e6e', - 1 - ); - fullProgressBar.draw(); - const progressBar = new CanvasProgressBar( - ctx, - { - x: avatarImage.width + 70, - y: avatarImage.height - 10, - height: 30, - width: 550 - }, - 'yellow', - currentLevelXPProgress / xpForNextLevel - ); - progressBar.draw(); - // Draw level data text - ctx.fillStyle = 'black' - ctx.fillText(`Level: ${userLevel} XP: $`, avatarImage.width + 70, avatarImage.height - 20) - // Return image in buffer form - return image.toBuffer(); - } */ - - private async getResponse(user: BushUser, guild: BushGuild): Promise<string> { - const userLevelRow = await Level.findOne({ where: { user: user.id, guild: guild.id } }); - if (userLevelRow) { - return `${user ? `${user.tag}'s` : 'Your'} level is ${userLevelRow.level} (${userLevelRow.xp} XP)`; - } else { - return `${user ? `${user.tag} does` : 'You do'} not have a level yet!`; - } - } - - public override async exec(message: BushMessage | BushSlashMessage, { user }: { user?: BushUser }): Promise<void> { - // await message.reply( - // new MessageAttachment( - // await this.getImage(user || message.author), - // 'lel.png' - // ) - // ); - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - await message.reply(await this.getResponse(user || message.author, message.guild!)); - } -} diff --git a/src/commands/moulberry-bush/rule.ts b/src/commands/moulberry-bush/rule.ts index bf44dad..0c1e435 100644 --- a/src/commands/moulberry-bush/rule.ts +++ b/src/commands/moulberry-bush/rule.ts @@ -131,7 +131,7 @@ export default class RuleCommand extends BushCommand { // If the original message was a reply -> imitate it message.reference?.messageId && !message.util.isSlash ? await message.channel.messages.fetch(message.reference.messageId).then(async (message) => { - await message.util!.reply({ embeds: [rulesEmbed], allowedMentions: AllowedMentions.users() }); + await message.reply({ embeds: [rulesEmbed], allowedMentions: AllowedMentions.users() }); }) : await message.util.send({ embeds: [rulesEmbed], allowedMentions: AllowedMentions.users() }) ); diff --git a/src/commands/utilities/decode.ts b/src/commands/utilities/decode.ts index a5a4c21..e48c644 100644 --- a/src/commands/utilities/decode.ts +++ b/src/commands/utilities/decode.ts @@ -95,8 +95,7 @@ export default class DecodeCommand extends BushCommand { message: BushMessage | AkairoMessage, { from, to, data }: { from: BufferEncoding; to: BufferEncoding; data: string } ): Promise<unknown> { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const encodeOrDecode = util.capitalizeFirstLetter(message?.util?.parsed?.alias || 'decoded'); + const encodeOrDecode = util.capitalizeFirstLetter(message?.util?.parsed?.alias ?? 'decoded'); const decodedEmbed = new MessageEmbed() .setTitle(`${encodeOrDecode} Information`) .addField('📥 Input', await util.inspectCleanRedactCodeblock(data)); diff --git a/src/commands/utilities/suicide.ts b/src/commands/utilities/suicide.ts new file mode 100644 index 0000000..0ec84a5 --- /dev/null +++ b/src/commands/utilities/suicide.ts @@ -0,0 +1,52 @@ +import { AllowedMentions, BushCommand, BushMessage, BushSlashMessage } from '@lib'; +import { MessageEmbed } from 'discord.js'; + +export default class TemplateCommand extends BushCommand { + public constructor() { + super('suicide', { + aliases: ['suicide'], + category: 'utilities', + description: { + content: 'Mental Health Resources. Credit to https://github.com/dexbiobot/Zeppelin.', + usage: 'suicide', + examples: ['suicide'] + }, + slash: true, + clientPermissions: ['SEND_MESSAGES'], + userPermissions: ['SEND_MESSAGES'] + }); + } + + public override async exec(message: BushMessage | BushSlashMessage): Promise<unknown> { + // stolen from https://github.com/dexbiobot/Zeppelin + const suicideEmbed = new MessageEmbed() + .setTitle('Mental Health Resources') + .setColor(util.colors.red) + .setAuthor( + 'Remember, You Matter <3', + 'https://media.discordapp.net/attachments/770256340639416320/854689949193076737/Medical_31-60_974.jpg?width=523&height=523' + ) + .addField( + '**National Suicide Prevention Hotline (U.S.):**', + `**Call:** 1-800-273-8255, available 24/7 for emotional support +**Text: HOME** to 741741 +https://suicidepreventionlifeline.org/chat/ + +Outside the U.S: Find a supportive resource on [this Wikipedia list of worldwide crisis hotlines](https://en.wikipedia.org/wiki/List_of_suicide_crisis_lines)` + ) + .addField( + '**More Support**', + `For Substance Abuse Support, Eating Disorder Support & Child Abuse and Domestic Violence: +[Click to go to Discord's Health & Safety Page](https://discord.com/safety/360044103771-Mental-health-on-Discord#h_01EGRGT08QSZ5BNCH2E9HN0NYV)` + ); + + return ( + // If the original message was a reply -> imitate it + (message as BushMessage).reference?.messageId && !message.util.isSlash && message.guild && message.channel + ? await message.channel.messages.fetch((message as BushMessage).reference!.messageId!).then(async (message1) => { + await message1.reply({ embeds: [suicideEmbed], allowedMentions: AllowedMentions.users(), target: message1 }); + }) + : await message.util.send({ embeds: [suicideEmbed], allowedMentions: AllowedMentions.users() }) + ); + } +} diff --git a/src/lib/extensions/discord-akairo/BushClient.ts b/src/lib/extensions/discord-akairo/BushClient.ts index ca5f325..2eaf3d3 100644 --- a/src/lib/extensions/discord-akairo/BushClient.ts +++ b/src/lib/extensions/discord-akairo/BushClient.ts @@ -26,6 +26,7 @@ import { contentWithDurationTypeCaster } from '../../../arguments/contentWithDur import { discordEmojiTypeCaster } from '../../../arguments/discordEmoji'; import { durationTypeCaster } from '../../../arguments/duration'; import { permissionTypeCaster } from '../../../arguments/permission'; +import { roleWithDurationTypeCaster } from '../../../arguments/roleWithDuation'; import { snowflakeTypeCaster } from '../../../arguments/snowflake'; import { UpdateCacheTask } from '../../../tasks/updateCache'; import { ActivePunishment } from '../../models/ActivePunishment'; @@ -264,7 +265,8 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re contentWithDuration: contentWithDurationTypeCaster, permission: permissionTypeCaster, snowflake: snowflakeTypeCaster, - discordEmoji: discordEmojiTypeCaster + discordEmoji: discordEmojiTypeCaster, + roleWithDuration: roleWithDurationTypeCaster }); // loads all the handlers const loaders = { diff --git a/src/lib/extensions/discord-akairo/BushClientUtil.ts b/src/lib/extensions/discord-akairo/BushClientUtil.ts index 9ed890a..9a5a07f 100644 --- a/src/lib/extensions/discord-akairo/BushClientUtil.ts +++ b/src/lib/extensions/discord-akairo/BushClientUtil.ts @@ -673,7 +673,8 @@ export class BushClientUtil extends ClientUtil { message: BushMessage | BushSlashMessage, embeds: MessageEmbed[], text: string | null = null, - deleteOnExit?: boolean + deleteOnExit?: boolean, + startOn?: number ): Promise<void> { const paginateEmojis = this.#paginateEmojis; if (deleteOnExit === undefined) deleteOnExit = true; @@ -687,7 +688,7 @@ export class BushClientUtil extends ClientUtil { }); const style = Constants.MessageButtonStyles.PRIMARY; - let curPage = 0; + let curPage = startOn ? startOn - 1 : undefined ?? 0; if (typeof embeds !== 'object') throw new Error('embeds must be an object'); const msg = (await message.util.reply({ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing @@ -999,7 +1000,7 @@ export class BushClientUtil extends ClientUtil { /** * Add or remove an item from an array. All duplicates will be removed. */ - public addOrRemoveFromArray(action: 'add' | 'remove', array: any[], value: any): any[] { + public addOrRemoveFromArray<T extends any>(action: 'add' | 'remove', array: T[], value: T): T[] { const set = new Set(array); action === 'add' ? set.add(value) : set.delete(value); return [...set]; @@ -1354,10 +1355,6 @@ export class BushClientUtil extends ClientUtil { return new Promise((resolve) => setTimeout(resolve, s * 1000)); } - camelToSnakeCase(str: string) { - return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); - } - //~ modified from https://stackoverflow.com/questions/31054910/get-functions-methods-of-a-class //~ answer by Bruno Grieder //~ public getMethods(obj: any): string { diff --git a/src/lib/extensions/discord-akairo/BushCommand.ts b/src/lib/extensions/discord-akairo/BushCommand.ts index 7ecb679..3a2c619 100644 --- a/src/lib/extensions/discord-akairo/BushCommand.ts +++ b/src/lib/extensions/discord-akairo/BushCommand.ts @@ -63,7 +63,8 @@ export type BaseBushArgumentType = | 'contentWithDuration' | 'permission' | 'snowflake' - | 'discordEmoji'; + | 'discordEmoji' + | 'roleWithDuration'; export type BushArgumentType = BaseBushArgumentType | RegExp; @@ -180,8 +181,7 @@ export class BushCommand extends Command { } super(id, options); this.options = options; - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - this.hidden = options.hidden || false; + this.hidden = options.hidden ?? false; this.restrictedChannels = options.restrictedChannels!; this.restrictedGuilds = options.restrictedGuilds!; this.completelyHide = options.completelyHide!; diff --git a/src/lib/extensions/discord.js/BushGuild.ts b/src/lib/extensions/discord.js/BushGuild.ts index 4fc27a7..2c3b4bd 100644 --- a/src/lib/extensions/discord.js/BushGuild.ts +++ b/src/lib/extensions/discord.js/BushGuild.ts @@ -73,8 +73,7 @@ export class BushGuild extends Guild { if (!bans.has(user)) notBanned = true; const unbanSuccess = await this.bans - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - .remove(user, `${moderator.tag} | ${options.reason || 'No reason provided.'}`) + .remove(user, `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`) .catch((e) => { if (e?.code === 'UNKNOWN_BAN') { notBanned = true; @@ -108,8 +107,7 @@ export class BushGuild extends Guild { const userObject = client.users.cache.get(user); - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - userObject?.send(`You have been unbanned from **${this}** for **${options.reason || 'No reason provided'}**.`); + userObject?.send(`You have been unbanned from **${this}** for **${options.reason ?? 'No reason provided'}**.`); if (notBanned) return 'user not banned'; return 'success'; diff --git a/src/lib/extensions/discord.js/BushGuildMember.ts b/src/lib/extensions/discord.js/BushGuildMember.ts index e596c82..6ce473a 100644 --- a/src/lib/extensions/discord.js/BushGuildMember.ts +++ b/src/lib/extensions/discord.js/BushGuildMember.ts @@ -182,7 +182,7 @@ export class BushGuildMember extends GuildMember { } #checkIfShouldAddRole(role: BushRole | Role): true | 'user hierarchy' | 'role managed' | 'client hierarchy' { - if (this.roles.highest.position <= role.position) { + if (this.roles.highest.position <= role.position && this.guild.ownerId !== this.id) { return 'user hierarchy'; } else if (role.managed) { return 'role managed'; diff --git a/src/lib/models/Guild.ts b/src/lib/models/Guild.ts index 4a5ede4..1974725 100644 --- a/src/lib/models/Guild.ts +++ b/src/lib/models/Guild.ts @@ -41,49 +41,49 @@ export interface GuildModelCreationAttributes { export const guildSettingsObj = { prefix: { name: 'Prefix', - description: 'description goes here', + description: 'The phrase required to trigger text commands in this server.', type: 'string', configurable: true }, autoPublishChannels: { name: 'Auto Publish Channels', - description: 'description goes here', + description: 'Channels were every message is automatically published.', type: 'channel-array', configurable: true }, welcomeChannel: { name: 'Welcome Channel', - description: 'description goes here', - type: 'channel-array', + description: 'The channel where the bot will send join and leave message.', + type: 'channel', configurable: true }, muteRole: { name: 'Mute Role', - description: 'description goes here', + description: 'The role assigned when muting someone.', type: 'role', configurable: true }, punishmentEnding: { name: 'Punishment Ending', - description: 'description goes here', + description: 'The message after punishment information to a user in a dm.', type: 'string', configurable: true }, lockdownChannels: { name: 'Lockdown Channels', - description: 'description goes here', + description: 'Channels that are locked down when a mass lockdown is specified.', type: 'channel-array', configurable: false // not implemented yet }, joinRoles: { name: 'Join Roles', - description: 'description goes here', + description: 'Roles assigned to users on join who do not have sticky role information.', type: 'role-array', configurable: true }, automodLogChannel: { name: 'Automod Log Channel', - description: 'description goes here', + description: 'The channel where all automod information is sent.', type: 'channel', configurable: true } @@ -359,9 +359,9 @@ __metadata: linkType: hard "@types/node@npm:*": - version: 16.7.4 - resolution: "@types/node@npm:16.7.4" - checksum: da7813e6c37e7813645a9d40de6d9f803fbadb2975748d307ec31d8e4f9baabccf49b667a39e4b1288d477ec7d34a339e8a41d8703a1d7ab0ec8eb2516073f27 + version: 16.7.6 + resolution: "@types/node@npm:16.7.6" + checksum: a8533386a1d4ca0ed67885413001af8789c63948df288f3d36e31bd8fccffacf5dffb95e190c8cd57bb40385f010fb9a30f596bad6bb26b2bb88737d54d8ed95 languageName: node linkType: hard @@ -797,6 +797,7 @@ __metadata: prettier: ^2.3.2 rimraf: ^3.0.2 sequelize: ^6.5.0 + simplify-number: ^1.0.0 source-map-support: ^0.5.19 tinycolor2: ^1.4.2 typescript: ^4.4.2 @@ -1116,12 +1117,12 @@ discord-akairo-message-util@NotEnoughUpdates/discord-akairo-message-util: discord-akairo@NotEnoughUpdates/discord-akairo: version: 8.2.2 - resolution: "discord-akairo@https://github.com/NotEnoughUpdates/discord-akairo.git#commit=0091ae5534d7eefbb401009a15aa7d6fd013f9b4" + resolution: "discord-akairo@https://github.com/NotEnoughUpdates/discord-akairo.git#commit=8c34349a3eb03164e34bcf538787cce259c76aa9" dependencies: discord-akairo-message-util: NotEnoughUpdates/discord-akairo-message-util lodash: ^4.17.21 source-map-support: ^0.5.19 - checksum: 1fcd67033576768a5b8bed52e15473795242860e5661d5f2a781acb0579e334d09fdf5f824e6eedcd7050fa5df88beadda309d19be503c7c3c5554917ccdbd4a + checksum: 7b71844b4955fed0f383b59e23770e526140459dc55b3a8af4707bd6be7a9295c2ac6812df47282a5b922a50f4569b597151f21f893ecce7ef9b1e8b87f88a64 languageName: node linkType: hard @@ -2903,6 +2904,13 @@ discord.js@NotEnoughUpdates/discord.js: languageName: node linkType: hard +"simplify-number@npm:^1.0.0": + version: 1.0.0 + resolution: "simplify-number@npm:1.0.0" + checksum: a8e1d85dcd390f5c8fc5b8b5f5d49c62590656540904e9809aef8dfc3e3846a9ebd14405e4a54849a1da3bdf7feaa2456886d32f0c27e1a942988dfb762da857 + languageName: node + linkType: hard + "slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" @@ -3369,8 +3377,8 @@ typescript@^4.4.2: linkType: hard "ws@npm:^7.4.4, ws@npm:^7.5.1": - version: 7.5.3 - resolution: "ws@npm:7.5.3" + version: 7.5.4 + resolution: "ws@npm:7.5.4" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ^5.0.2 @@ -3379,7 +3387,7 @@ typescript@^4.4.2: optional: true utf-8-validate: optional: true - checksum: 423dc0d859fa74020f5555140905b862470a60ea1567bb9ad55a087263d7718b9c94f69678be1cee9868925c570f1e6fc79d09f90c39057bc63fa2edbb2c547b + checksum: 48582e4feb1fc6b6b977a0ee6136e5cd1c6a14bc5cb6ce5acf596652b34be757cdf0c225235b3263d56d057bc5d6e528dbe27fc88a3d09828aa803c6696f4b2c languageName: node linkType: hard |