diff options
Diffstat (limited to 'src')
65 files changed, 1340 insertions, 527 deletions
diff --git a/src/arguments/duration.ts b/src/arguments/duration.ts new file mode 100644 index 0000000..a2f7751 --- /dev/null +++ b/src/arguments/duration.ts @@ -0,0 +1,55 @@ +import { BushArgumentTypeCaster } from '../lib/extensions/BushArgumentTypeCaster'; +import { BushMessage } from '../lib/extensions/BushMessage'; + +// Stolen from @Mzato0001 (pr to discord akairo that hasn't been merged yet) +const TimeUnits = { + years: { + label: '(?:years?|y)', + value: 1000 * 60 * 60 * 24 * 365 + }, + months: { + label: '(?:months?|mo)', + value: 1000 * 60 * 60 * 24 * 30 + }, + weeks: { + label: '(?:weeks?|w)', + value: 1000 * 60 * 60 * 24 * 7 + }, + days: { + label: '(?:days?|d)', + value: 1000 * 60 * 60 * 24 + }, + hours: { + label: '(?:hours?|hrs?|h)', + value: 1000 * 60 * 60 + }, + minutes: { + label: '(?:minutes?|mins?|m)', + value: 1000 * 60 + }, + seconds: { + label: '(?:seconds?|secs?|s)', + value: 1000 + }, + milliseconds: { + label: '(?:milliseconds?|msecs?|ms)', + value: 1 + } +}; +export const durationTypeCaster: BushArgumentTypeCaster = async (_message: BushMessage, phrase): Promise<number> => { + if (!phrase) return null; + + const regexString = Object.entries(TimeUnits) + .map(([name, { label }]) => String.raw`(?:(?<${name}>-?(?:\d+)?\.?\d+) *${label})?`) + .join('\\s*'); + const match = new RegExp(`^${regexString}$`, 'i').exec(phrase); + if (!match) return null; + + let milliseconds = 0; + for (const key in match.groups) { + const value = Number(match.groups[key] || 0); + milliseconds += value * TimeUnits[key].value; + } + + return milliseconds; +}; diff --git a/src/commands/config/muteRole.ts b/src/commands/config/muteRole.ts new file mode 100644 index 0000000..f51c5ce --- /dev/null +++ b/src/commands/config/muteRole.ts @@ -0,0 +1,57 @@ +import { Role } from 'discord.js'; +import { BushCommand } from '../../lib/extensions/BushCommand'; +import { BushSlashMessage } from '../../lib/extensions/BushInteractionMessage'; +import { BushMessage } from '../../lib/extensions/BushMessage'; +import { Guild } from '../../lib/models'; +import AllowedMentions from '../../lib/utils/AllowedMentions'; + +export default class MuteRoleCommand extends BushCommand { + constructor() { + super('muteRole', { + aliases: ['muterole'], + category: 'config', + description: { + content: 'Set the prefix of the current server (resets to default if prefix is not given)', + usage: 'prefix [prefix]', + examples: ['prefix', 'prefix +'] + }, + clientPermissions: ['SEND_MESSAGES'], + userPermissions: ['SEND_MESSAGES', 'MANAGE_GUILD'], + args: [ + { + id: 'role', + type: 'role', + prompt: { + start: "What would you like to set the server's mute role to?", + retry: '{error} Choose a valid role.', + optional: false + } + } + ], + slash: true, + slashOptions: [ + { + type: 'ROLE', + name: 'role', + description: 'The mute role for this server.', + required: true + } + ] + }); + } + + async exec(message: BushMessage | BushSlashMessage, args: { role: Role }): Promise<void> { + let row = await Guild.findByPk(message.guild.id); + if (!row) { + row = Guild.build({ + id: message.guild.id + }); + } + row.muteRole = args.role.id; + await row.save(); + await message.util.send({ + content: `${this.client.util.emojis.success} Changed the mute role to <@&${args.role.id}>.`, + allowedMentions: AllowedMentions.none() + }); + } +} diff --git a/src/commands/config/prefix.ts b/src/commands/config/prefix.ts index 1326426..5b73a1a 100644 --- a/src/commands/config/prefix.ts +++ b/src/commands/config/prefix.ts @@ -1,6 +1,6 @@ -import { Message } from 'discord.js'; import { BushCommand } from '../../lib/extensions/BushCommand'; import { BushSlashMessage } from '../../lib/extensions/BushInteractionMessage'; +import { BushMessage } from '../../lib/extensions/BushMessage'; import { Guild } from '../../lib/models'; export default class PrefixCommand extends BushCommand { @@ -11,10 +11,16 @@ export default class PrefixCommand extends BushCommand { args: [ { id: 'prefix', - type: 'string' + type: 'string', + prompt: { + start: 'What would you like the new prefix to be?', + retry: '{error} Choose a valid prefix', + optional: true + } } ], - userPermissions: ['MANAGE_GUILD'], + clientPermissions: ['SEND_MESSAGES'], + userPermissions: ['SEND_MESSAGES', 'MANAGE_GUILD'], description: { content: 'Set the prefix of the current server (resets to default if prefix is not given)', usage: 'prefix [prefix]', @@ -32,7 +38,7 @@ export default class PrefixCommand extends BushCommand { }); } - async exec(message: Message | BushSlashMessage, { prefix }: { prefix?: string }): Promise<void> { + async exec(message: BushMessage | BushSlashMessage, { prefix }: { prefix?: string }): Promise<void> { let row = await Guild.findByPk(message.guild.id); if (!row) { row = Guild.build({ @@ -41,7 +47,7 @@ export default class PrefixCommand extends BushCommand { } await row.update({ prefix: prefix || this.client.config.prefix }); if (prefix) { - await message.util.send(`${this.client.util.emojis.success} changed prefix from \`${prefix}\``); + await message.util.send(`${this.client.util.emojis.success} changed prefix from \`${prefix}\` to `); } else { await message.util.send(`${this.client.util.emojis.success} reset prefix to \`${this.client.config.prefix}\``); } diff --git a/src/commands/config/welcomeChannel.ts b/src/commands/config/welcomeChannel.ts new file mode 100644 index 0000000..72e55f1 --- /dev/null +++ b/src/commands/config/welcomeChannel.ts @@ -0,0 +1,49 @@ +import { User } from 'discord.js'; +import { BushCommand } from '../../lib/extensions/BushCommand'; +import { BushMessage } from '../../lib/extensions/BushMessage'; +import { Global } from '../../lib/models'; + +export default class WelcomeChannelCommand extends BushCommand { + public constructor() { + super('welcomeChannel', { + aliases: ['welcomechannel', 'wc'], + category: 'config', + description: { + content: 'Configure the what channel you want the bot to send a message in when someone joins the server.', + usage: 'welcomechannel [channel]', + examples: ['welcomechannel #welcome'] + }, + clientPermissions: ['SEND_MESSAGES'], + ownerOnly: true + }); + } + public async exec(message: BushMessage, args: { action: 'add' | 'remove'; user: User }): Promise<unknown> { + if (!this.client.config.owners.includes(message.author.id)) + return await message.util.reply(`${this.client.util.emojis.error} Only my developers can run this command...`); + + const superUsers = (await Global.findByPk(this.client.config.dev ? 'development' : 'production')).superUsers; + let success; + if (args.action === 'add') { + if (superUsers.includes(args.user.id)) { + return message.util.reply(`${this.client.util.emojis.warn} \`${args.user.tag}\` is already a superuser.`); + } + success = await this.client.util.insertOrRemoveFromGlobal('add', 'superUsers', args.user.id).catch(() => false); + } else { + if (!superUsers.includes(args.user.id)) { + return message.util.reply(`${this.client.util.emojis.warn} \`${args.user.tag}\` is not superuser.`); + } + success = await this.client.util.insertOrRemoveFromGlobal('remove', 'superUsers', args.user.id).catch(() => false); + } + if (success) { + const responses = [args.action == 'remove' ? `` : 'made', args.action == 'remove' ? 'is no longer' : '']; + return message.util.reply( + `${this.client.util.emojis.success} ${responses[0]} \`${args.user.tag}\` ${responses[1]} a superuser.` + ); + } else { + const response = [args.action == 'remove' ? `removing` : 'making', args.action == 'remove' ? `from` : 'to']; + return message.util.reply( + `${this.client.util.emojis.error} There was an error ${response[0]} \`${args.user.tag}\` ${response[1]} the superuser list.` + ); + } + } +} diff --git a/src/commands/dev/eval.ts b/src/commands/dev/eval.ts index d2fe432..310a2c5 100644 --- a/src/commands/dev/eval.ts +++ b/src/commands/dev/eval.ts @@ -10,7 +10,7 @@ import { BushMessage } from '../../lib/extensions/BushMessage'; const clean = (text) => { if (typeof text === 'string') { - return (text = Util.cleanCodeBlockContent(text)); + return Util.cleanCodeBlockContent(text); } else return text; }; const sh = promisify(exec); @@ -186,7 +186,7 @@ export default class EvalCommand extends BushCommand { { Global } = await import('../../lib/models/Global'), { Guild } = await import('../../lib/models/Guild'), { Level } = await import('../../lib/models/Level'), - { Modlog } = await import('../../lib/models/Modlog'), + { ModLog } = await import('../../lib/models/ModLog'), { StickyRole } = await import('../../lib/models/StickyRole'); if (code[code.lang].replace(/ /g, '').includes('9+10' || '10+9')) { output = 21; diff --git a/src/commands/dev/reload.ts b/src/commands/dev/reload.ts index 94e31b0..f5fee88 100644 --- a/src/commands/dev/reload.ts +++ b/src/commands/dev/reload.ts @@ -1,6 +1,5 @@ import { stripIndent } from 'common-tags'; import { Message } from 'discord.js'; -import { SlashCommandOption } from '../../lib/extensions/BushClientUtil'; import { BushCommand } from '../../lib/extensions/BushCommand'; import { BushSlashMessage } from '../../lib/extensions/BushInteractionMessage'; @@ -51,11 +50,7 @@ export default class ReloadCommand extends BushCommand { } } - public async exec(message: Message, { fast }: { fast: boolean }): Promise<void> { + public async exec(message: Message | BushSlashMessage, { fast }: { fast: boolean }): Promise<void> { await message.util.send(await this.getResponse(fast)); } - - public async execSlash(message: BushSlashMessage, { fast }: { fast: SlashCommandOption<boolean> }): Promise<void> { - await message.interaction.reply(await this.getResponse(fast?.value)); - } } diff --git a/src/commands/dev/setLevel.ts b/src/commands/dev/setLevel.ts index ca555db..f536109 100644 --- a/src/commands/dev/setLevel.ts +++ b/src/commands/dev/setLevel.ts @@ -1,7 +1,5 @@ import { Message, User } from 'discord.js'; -import { SlashCommandOption } from '../../lib/extensions/BushClientUtil'; import { BushCommand } from '../../lib/extensions/BushCommand'; -import { BushSlashMessage } from '../../lib/extensions/BushInteractionMessage'; import { Level } from '../../lib/models'; import AllowedMentions from '../../lib/utils/AllowedMentions'; @@ -71,14 +69,4 @@ export default class SetLevelCommand extends BushCommand { allowedMentions: AllowedMentions.none() }); } - - async execSlash( - message: BushSlashMessage, - { user, level }: { user: SlashCommandOption<void>; level: SlashCommandOption<number> } - ): Promise<void> { - await message.interaction.reply({ - content: await this.setLevel(user.user, level.value), - allowedMentions: AllowedMentions.none() - }); - } } diff --git a/src/commands/dev/superUser.ts b/src/commands/dev/superUser.ts index c3ed0b0..4562e6f 100644 --- a/src/commands/dev/superUser.ts +++ b/src/commands/dev/superUser.ts @@ -2,7 +2,7 @@ import { Constants } from 'discord-akairo'; import { User } from 'discord.js'; import { BushCommand } from '../../lib/extensions/BushCommand'; import { BushMessage } from '../../lib/extensions/BushMessage'; -import { Global } from '../../lib/models/Global'; +import { Global } from '../../lib/models'; export default class SuperUserCommand extends BushCommand { public constructor() { diff --git a/src/commands/info/botInfo.ts b/src/commands/info/botInfo.ts index 120527d..3db4151 100644 --- a/src/commands/info/botInfo.ts +++ b/src/commands/info/botInfo.ts @@ -1,7 +1,6 @@ import { Message, MessageEmbed } from 'discord.js'; import { duration } from 'moment'; import { BushCommand } from '../../lib/extensions/BushCommand'; -import { BushSlashMessage } from '../../lib/extensions/BushInteractionMessage'; export default class BotInfoCommand extends BushCommand { constructor() { @@ -13,11 +12,13 @@ export default class BotInfoCommand extends BushCommand { usage: 'botinfo', examples: ['botinfo'] }, - slash: true + slash: true, + clientPermissions: ['SEND_MESSAGES', 'EMBED_LINKS'], + userPermissions: ['SEND_MESSAGES'] }); } - private async generateEmbed(): Promise<MessageEmbed> { + public async exec(message: Message): Promise<void> { const owners = (await this.client.util.mapIDs(this.client.ownerID)).map((u) => u.tag).join('\n'); const currentCommit = (await this.client.util.shell('git rev-parse HEAD')).stdout.replace('\n', ''); const repoUrl = (await this.client.util.shell('git remote get-url origin')).stdout.replace('\n', ''); @@ -44,14 +45,6 @@ export default class BotInfoCommand extends BushCommand { } ]) .setTimestamp(); - return embed; - } - - public async exec(message: Message): Promise<void> { - await message.util.send({ embeds: [await this.generateEmbed()] }); - } - - public async execSlash(message: BushSlashMessage): Promise<void> { - await message.interaction.reply({ embeds: [await this.generateEmbed()] }); + await message.util.reply({ embeds: [embed] }); } } diff --git a/src/commands/info/help.ts b/src/commands/info/help.ts index 0efc6b3..8969efc 100644 --- a/src/commands/info/help.ts +++ b/src/commands/info/help.ts @@ -14,6 +14,7 @@ export default class HelpCommand extends BushCommand { examples: ['help prefix'] }, clientPermissions: ['EMBED_LINKS'], + args: [ { id: 'command', @@ -31,15 +32,15 @@ export default class HelpCommand extends BushCommand { flag: '--hidden' } ], + slash: true, slashOptions: [ { type: 'STRING', name: 'command', - description: `The command you would like to find information about.`, + description: 'The command you would like to find information about.', required: false } - ], - slash: true + ] }); } @@ -79,8 +80,7 @@ export default class HelpCommand extends BushCommand { if (command.superUserOnly && !isSuperUser) { return false; } - if (command.restrictedGuilds?.includes(message.guild.id) == !true && !args.showHidden) return false; - return true; + return !(command.restrictedGuilds?.includes(message.guild.id) == false && !args.showHidden); }); const categoryNice = category.id .replace(/(\b\w)/gi, (lc): string => lc.toUpperCase()) diff --git a/src/commands/info/ping.ts b/src/commands/info/ping.ts index e80cfb3..3038658 100644 --- a/src/commands/info/ping.ts +++ b/src/commands/info/ping.ts @@ -12,6 +12,8 @@ export default class PingCommand extends BushCommand { usage: 'ping', examples: ['ping'] }, + clientPermissions: ['SEND_MESSAGES', 'EMBED_LINKS'], + userPermissions: ['SEND_MESSAGES'], slash: true }); } diff --git a/src/commands/info/pronouns.ts b/src/commands/info/pronouns.ts index 79baeef..2175233 100644 --- a/src/commands/info/pronouns.ts +++ b/src/commands/info/pronouns.ts @@ -1,8 +1,6 @@ import { CommandInteraction, Message, MessageEmbed, User } from 'discord.js'; import got, { HTTPError } from 'got'; -import { SlashCommandOption } from '../../lib/extensions/BushClientUtil'; import { BushCommand } from '../../lib/extensions/BushCommand'; -import { BushSlashMessage } from '../../lib/extensions/BushInteractionMessage'; export const pronounMapping = { unspecified: 'Unspecified', @@ -55,7 +53,6 @@ export default class PronounsCommand extends BushCommand { required: false } ], - slashEphemeral: true, // I'll add dynamic checking to this later slash: true }); } @@ -107,8 +104,4 @@ export default class PronounsCommand extends BushCommand { const u = user || message.author; await this.sendResponse(message, u, u.id === message.author.id); } - async execSlash(message: BushSlashMessage, { user }: { user?: SlashCommandOption<void> }): Promise<void> { - const u = user?.user || message.author; - await this.sendResponse(message.interaction, u, u.id === message.author.id); - } } diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 692691e..7ce222a 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -1,18 +1,17 @@ +import { Argument } from 'discord-akairo'; import { CommandInteraction, Message, User } from 'discord.js'; import moment from 'moment'; -import { SlashCommandOption } from '../../lib/extensions/BushClientUtil'; import { BushCommand } from '../../lib/extensions/BushCommand'; -import { BushSlashMessage } from '../../lib/extensions/BushInteractionMessage'; -import { Ban, Guild, Modlog, ModlogType } from '../../lib/models'; +import { Ban, Guild, ModLog, ModLogType } from '../../lib/models'; -const durationAliases: Record<string, string[]> = { +/* const durationAliases: Record<string, string[]> = { weeks: ['w', 'weeks', 'week', 'wk', 'wks'], days: ['d', 'days', 'day'], hours: ['h', 'hours', 'hour', 'hr', 'hrs'], minutes: ['m', 'min', 'mins', 'minutes', 'minute'], months: ['mo', 'month', 'months'] }; -const durationRegex = /(?:(\d+)(d(?:ays?)?|h(?:ours?|rs?)?|m(?:inutes?|ins?)?|mo(?:nths?)?|w(?:eeks?|ks?)?)(?: |$))/g; +const durationRegex = /(?:(\d+)(d(?:ays?)?|h(?:ours?|rs?)?|m(?:inutes?|ins?)?|mo(?:nths?)?|w(?:eeks?|ks?)?)(?: |$))/g; */ export default class BanCommand extends BushCommand { constructor() { @@ -25,15 +24,21 @@ export default class BanCommand extends BushCommand { type: 'user', prompt: { start: 'What user would you like to ban?', - retry: 'Invalid response. What user would you like to ban?' + retry: '{error} Choose a valid user to ban.' } }, { id: 'reason', - match: 'rest' + match: 'restContent', + prompt: { + start: 'Why would you like to ban this user?', + retry: '{error} Choose a ban reason.', + optional: true + } }, { id: 'time', + type: 'duration', match: 'option', flag: '--time' } @@ -41,27 +46,27 @@ export default class BanCommand extends BushCommand { clientPermissions: ['BAN_MEMBERS'], userPermissions: ['BAN_MEMBERS'], description: { - content: 'Ban a member and log it in modlogs (with optional time to unban)', + content: 'Ban a member from the server.', usage: 'ban <member> <reason> [--time]', - examples: ['ban @Tyman being cool', 'ban @Tyman being cool --time 7days'] + examples: ['ban @user bad --time 69d'] }, slashOptions: [ { type: 'USER', name: 'user', - description: 'The user to ban', + description: 'Who would you like to ban?', required: true }, { type: 'STRING', name: 'reason', - description: 'The reason to show in modlogs and audit log', + description: 'Why are they getting banned?', required: false }, { type: 'STRING', name: 'time', - description: 'The time the user should be banned for (default permanent)', + description: 'How long should they be banned for?', required: false } ], @@ -72,12 +77,12 @@ export default class BanCommand extends BushCommand { message: Message | CommandInteraction, user: User, reason?: string, - time?: string + time?: number ): AsyncIterable<string> { const duration = moment.duration(); - let modlogEnry: Modlog; + let modLogEntry: ModLog; let banEntry: Ban; - const translatedTime: string[] = []; + // const translatedTime: string[] = []; // Create guild entry so postgres doesn't get mad when I try and add a modlog entry await Guild.findOrCreate({ where: { @@ -88,62 +93,58 @@ export default class BanCommand extends BushCommand { } }); try { - try { - if (time) { - const parsed = [...time.matchAll(durationRegex)]; + if (time) { + duration.add(time); + /* const parsed = [...time.matchAll(durationRegex)]; if (parsed.length < 1) { - yield 'Invalid time.'; + yield `${this.client.util.emojis.error} Invalid time.`; return; } for (const part of parsed) { const translated = Object.keys(durationAliases).find((k) => durationAliases[k].includes(part[2])); translatedTime.push(part[1] + ' ' + translated); duration.add(Number(part[1]), translated as 'weeks' | 'days' | 'hours' | 'months' | 'minutes'); - } - modlogEnry = Modlog.build({ - user: user.id, - guild: message.guild.id, - reason, - type: ModlogType.TEMPBAN, - duration: duration.asMilliseconds(), - moderator: message instanceof CommandInteraction ? message.user.id : message.author.id - }); - banEntry = Ban.build({ - user: user.id, - guild: message.guild.id, - reason, - expires: new Date(new Date().getTime() + duration.asMilliseconds()), - modlog: modlogEnry.id - }); - } else { - modlogEnry = Modlog.build({ - user: user.id, - guild: message.guild.id, - reason, - type: ModlogType.BAN, - moderator: message instanceof CommandInteraction ? message.user.id : message.author.id - }); - banEntry = Ban.build({ - user: user.id, - guild: message.guild.id, - reason, - modlog: modlogEnry.id - }); - } - await modlogEnry.save(); - await banEntry.save(); - } catch (e) { - this.client.console.error(`BanCommand`, `Error saving to database. ${e?.stack}`); - yield `${this.client.util.emojis.error} Error saving to database. Please report this to a developer.`; - return; + } */ + modLogEntry = ModLog.build({ + user: user.id, + guild: message.guild.id, + reason, + type: ModLogType.TEMP_BAN, + duration: duration.asMilliseconds(), + moderator: message instanceof CommandInteraction ? message.user.id : message.author.id + }); + banEntry = Ban.build({ + user: user.id, + guild: message.guild.id, + reason, + expires: new Date(new Date().getTime() + duration.asMilliseconds()), + modlog: modLogEntry.id + }); + } else { + modLogEntry = ModLog.build({ + user: user.id, + guild: message.guild.id, + reason, + type: ModLogType.BAN, + moderator: message instanceof CommandInteraction ? message.user.id : message.author.id + }); + banEntry = Ban.build({ + user: user.id, + guild: message.guild.id, + reason, + modlog: modLogEntry.id + }); } + await modLogEntry.save(); + await banEntry.save(); + try { await user.send( - `You were banned in ${message.guild.name} ${ - translatedTime.length >= 1 ? `for ${translatedTime.join(', ')}` : 'permanently' - } with reason \`${reason || 'No reason given'}\`` + `You were banned in ${message.guild.name} ${duration ? duration.humanize() : 'permanently'} with reason \`${ + reason || 'No reason given' + }\`` ); - } catch (e) { + } catch { yield `${this.client.util.emojis.warn} Unable to dm user`; } await message.guild.members.ban(user, { @@ -152,35 +153,22 @@ export default class BanCommand extends BushCommand { }` }); yield `${this.client.util.emojis.success} Banned <@!${user.id}> ${ - translatedTime.length >= 1 ? `for ${translatedTime.join(', ')}` : 'permanently' + duration ? duration.humanize() : 'permanently' } with reason \`${reason || 'No reason given'}\``; } catch { yield `${this.client.util.emojis.error} Error banning :/`; await banEntry.destroy(); - await modlogEnry.destroy(); + await modLogEntry.destroy(); return; } } - async exec(message: Message, { user, reason, time }: { user: User; reason?: string; time?: string }): Promise<void> { + async exec(message: Message, { user, reason, time }: { user: User; reason?: string; time?: number | string }): Promise<void> { + if (typeof time === 'string') { + time = (await Argument.cast('duration', this.client.commandHandler.resolver, message, time)) as number; + //// time = this.client.commandHandler.resolver.type('duration') + } for await (const response of this.genResponses(message, user, reason, time)) { await message.util.send(response); } } - - async execSlash( - message: BushSlashMessage, - { - user, - reason, - time - }: { - user: SlashCommandOption<undefined>; - reason: SlashCommandOption<string>; - time: SlashCommandOption<string>; - } - ): Promise<void> { - for await (const response of this.genResponses(message.interaction, user.user, reason?.value, time?.value)) { - await message.reply(response); - } - } } diff --git a/src/tasks/unrole.ts b/src/commands/moderation/block.ts index e69de29..e69de29 100644 --- a/src/tasks/unrole.ts +++ b/src/commands/moderation/block.ts diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index d00ae55..eed3038 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -1,7 +1,7 @@ import { CommandInteraction, GuildMember, Message } from 'discord.js'; import { BushCommand } from '../../lib/extensions/BushCommand'; import { BushSlashMessage } from '../../lib/extensions/BushInteractionMessage'; -import { Guild, Modlog, ModlogType } from '../../lib/models'; +import { Guild, ModLog, ModLogType } from '../../lib/models'; export default class KickCommand extends BushCommand { constructor() { @@ -14,17 +14,19 @@ export default class KickCommand extends BushCommand { type: 'member', prompt: { start: 'What user would you like to kick?', - retry: 'Invalid response. What user would you like to kick?' + retry: '{error} Choose a valid user to kick.' } }, { - id: 'reason' + id: 'reason', + type: 'string', + match: 'restContent' } ], clientPermissions: ['KICK_MEMBERS'], userPermissions: ['KICK_MEMBERS'], description: { - content: 'Kick a member and log it in modlogs', + content: 'Kick a member from the server.', usage: 'kick <member> <reason>', examples: ['kick @Tyman being cool'] }, @@ -51,7 +53,7 @@ export default class KickCommand extends BushCommand { user: GuildMember, reason?: string ): AsyncIterable<string> { - let modlogEnry: Modlog; + let modlogEnry: ModLog; // Create guild entry so postgres doesn't get mad when I try and add a modlog entry await Guild.findOrCreate({ where: { @@ -62,16 +64,16 @@ export default class KickCommand extends BushCommand { } }); try { - modlogEnry = Modlog.build({ + modlogEnry = ModLog.build({ user: user.id, guild: message.guild.id, moderator: message instanceof Message ? message.author.id : message.user.id, - type: ModlogType.KICK, + type: ModLogType.KICK, reason }); await modlogEnry.save(); } catch (e) { - this.client.console.error(`BanCommand`, `Error saving to database. ${e?.stack}`); + this.client.console.error(`KickCommand`, `Error saving to database. ${e?.stack}`); yield `${this.client.util.emojis.error} Error saving to database. Please report this to a developer.`; return; } diff --git a/src/commands/moderation/modlog.ts b/src/commands/moderation/modlog.ts index 862a26d..e32df42 100644 --- a/src/commands/moderation/modlog.ts +++ b/src/commands/moderation/modlog.ts @@ -3,7 +3,7 @@ import { Argument } from 'discord-akairo'; import { Message, MessageEmbed } from 'discord.js'; import moment from 'moment'; import { BushCommand } from '../../lib/extensions/BushCommand'; -import { Modlog } from '../../lib/models'; +import { ModLog } from '../../lib/models'; export default class ModlogCommand extends BushCommand { constructor() { @@ -35,7 +35,8 @@ export default class ModlogCommand extends BushCommand { id: 'search', type: Argument.union('user', 'string'), prompt: { - start: 'What modlog id or user would you like to see?' + start: 'What modlog id or user would you like to see?', + retry: '{error} Choose a valid modlog id or user.' } }; if (typeof search === 'string') return { search, page: null }; @@ -45,7 +46,7 @@ export default class ModlogCommand extends BushCommand { type: 'number', prompt: { start: 'What page?', - retry: 'Not a number. What page?', + retry: '{error} Choose a valid page to view.', optional: true } }; @@ -55,7 +56,7 @@ export default class ModlogCommand extends BushCommand { async exec(message: Message, { search, page }: { search: string; page: number }): Promise<void> { const foundUser = await this.client.util.resolveUserAsync(search); if (foundUser) { - const logs = await Modlog.findAll({ + const logs = await ModLog.findAll({ where: { guild: message.guild.id, user: foundUser.id @@ -65,24 +66,25 @@ export default class ModlogCommand extends BushCommand { const niceLogs: string[] = []; for (const log of logs) { niceLogs.push(stripIndent` - ID: ${log.id} - Type: ${log.type.toLowerCase()} - User: <@!${log.user}> (${log.user}) - Moderator: <@!${log.moderator}> (${log.moderator}) - Duration: ${log.duration ? moment.duration(log.duration, 'milliseconds').humanize() : 'N/A'} - Reason: ${log.reason || 'None given'} - ${this.client.util.ordinal(logs.indexOf(log) + 1)} action + **Case ID**: ${log.id} + **Type**: ${log.type.toLowerCase()} + **User**: <@!${log.user}> (${log.user}) + **Moderator**: <@!${log.moderator}> (${log.moderator}) + **Duration**: ${log.duration ? moment.duration(log.duration, 'milliseconds').humanize() : 'N/A'} + **Reason**: ${log.reason || 'None given'} + **${this.client.util.ordinal(logs.indexOf(log) + 1)}** action `); } const chunked: string[][] = this.client.util.chunk(niceLogs, 3); const embedPages = chunked.map( (e, i) => new MessageEmbed({ - title: `Modlogs page ${i + 1}`, - description: e.join('\n-------------------------------------------------------\n'), + title: foundUser.tag, + description: e.join('\n**---------------------------**\n'), footer: { text: `Page ${i + 1}/${chunked.length}` - } + }, + color: this.client.util.colors.default }) ); if (page) { @@ -93,15 +95,15 @@ export default class ModlogCommand extends BushCommand { return; } } else if (search) { - const entry = await Modlog.findByPk(search); + const entry = await ModLog.findByPk(search); if (!entry) { - await message.util.send('That modlog does not exist.'); + await message.util.send(`${this.client.util.emojis.error} That modlog does not exist.`); return; } await message.util.send({ embeds: [ new MessageEmbed({ - title: `Modlog ${entry.id}`, + title: `${entry.id}`, fields: [ { name: 'Type', diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts new file mode 100644 index 0000000..9b6ad70 --- /dev/null +++ b/src/commands/moderation/mute.ts @@ -0,0 +1,166 @@ +import { Argument } from 'discord-akairo'; +import { CommandInteraction, Message, User } from 'discord.js'; +import moment from 'moment'; +import { BushCommand } from '../../lib/extensions/BushCommand'; +import { Guild, ModLog, ModLogType, Mute } from '../../lib/models'; + +export default class MuteCommand extends BushCommand { + constructor() { + super('mute', { + aliases: ['mute'], + category: 'moderation', + args: [ + { + id: 'user', + type: 'user', + prompt: { + start: 'What user would you like to mute?', + retry: '{error} Choose a valid user to mute.' + } + }, + { + id: 'reason', + match: 'separate', + prompt: { + start: 'Why would you like to mute this user?', + retry: '{error} Choose a mute reason.', + optional: true + } + }, + { + id: 'time', + type: 'duration', + match: 'option', + flag: '--time' + } + ], + clientPermissions: ['MANAGE_ROLES'], + userPermissions: ['MANAGE_MESSAGES'], + description: { + content: 'Mute a user.', + usage: 'mute <member> <reason> [--time]', + examples: ['mute @user bad boi --time 1h'] + }, + slashOptions: [ + { + type: 'USER', + name: 'user', + description: 'The user to mute.', + required: true + }, + { + type: 'STRING', + name: 'reason', + description: 'Why the user is getting muted.', + required: false + }, + { + type: 'STRING', + name: 'time', + description: 'How long the user should be muted for.', + required: false + } + ], + slash: true + }); + } + async *genResponses( + message: Message | CommandInteraction, + user: User, + reason?: string, + time?: number + ): AsyncIterable<string> { + const duration = moment.duration(time); + let modlogEnry: ModLog; + let muteEntry: Mute; + // Create guild entry so postgres doesn't get mad when I try and add a modlog entry + await Guild.findOrCreate({ + where: { + id: message.guild.id + }, + defaults: { + id: message.guild.id + } + }); + try { + const muteRole = (await Guild.findByPk(message.guild.id)).get('muteRole'); + try { + if (time) { + modlogEnry = ModLog.build({ + user: user.id, + guild: message.guild.id, + reason, + type: ModLogType.TEMP_MUTE, + duration: duration.asMilliseconds(), + moderator: message instanceof CommandInteraction ? message.user.id : message.author.id + }); + muteEntry = Mute.build({ + user: user.id, + guild: message.guild.id, + reason, + expires: new Date(new Date().getTime() + duration.asMilliseconds()), + modlog: modlogEnry.id + }); + } else { + modlogEnry = ModLog.build({ + user: user.id, + guild: message.guild.id, + reason, + type: ModLogType.MUTE, + moderator: message instanceof CommandInteraction ? message.user.id : message.author.id + }); + muteEntry = Mute.build({ + user: user.id, + guild: message.guild.id, + reason, + modlog: modlogEnry.id + }); + } + await modlogEnry.save(); + await muteEntry.save(); + } catch (e) { + this.client.console.error(`MuteCommand`, `Error saving to database. ${e?.stack}`); + yield `${this.client.util.emojis.error} Error saving to database. Please report this to a developer.`; + return; + } + try { + await user.send( + `You were muted in ${message.guild.name} ${time ? `for ${duration.humanize()}` : 'permanently'} with reason \`${ + reason || 'No reason given' + }\`` + ); + } catch (e) { + yield `${this.client.util.emojis.warn} Unable to dm user`; + } + await ( + await message.guild.members.fetch(user) + ).roles.add( + muteRole, + `Muted by ${message instanceof CommandInteraction ? message.user.tag : message.author.tag} with ${ + reason ? `reason ${reason}` : 'no reason' + }` + ); + yield `${this.client.util.emojis.success} muted <@!${user.id}> ${ + time ? `for ${duration.humanize()}` : 'permanently' + } with reason \`${reason || 'No reason given'}\``; + } catch { + yield `${this.client.util.emojis.error} Error muting :/`; + await muteEntry.destroy(); + await modlogEnry.destroy(); + return; + } + } + async exec( + message: Message, + { user, reason, time }: { user: User; reason?: string[]; time?: string | number } + ): Promise<void> { + this.client.console.debug(reason); + + if (typeof time === 'string') { + time = (await Argument.cast('duration', this.client.commandHandler.resolver, message, time)) as number; + } + for await (const response of this.genResponses(message, user, reason.join(' '), time)) { + await message.util.sendNew(response); + } + } +} diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/unban.ts new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/commands/moderation/unban.ts diff --git a/src/commands/moderation/unblock.ts b/src/commands/moderation/unblock.ts new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/commands/moderation/unblock.ts diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/commands/moderation/unmute.ts diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index 3410d81..d48049b 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -1,6 +1,6 @@ import { GuildMember, Message } from 'discord.js'; import { BushCommand } from '../../lib/extensions/BushCommand'; -import { Guild, Modlog, ModlogType } from '../../lib/models'; +import { Guild, ModLog, ModLogType } from '../../lib/models'; export default class WarnCommand extends BushCommand { public constructor() { @@ -36,11 +36,11 @@ export default class WarnCommand extends BushCommand { } }); try { - const entry = Modlog.build({ + const entry = ModLog.build({ user: member.id, guild: message.guild.id, moderator: message.author.id, - type: ModlogType.WARN, + type: ModLogType.WARN, reason }); await entry.save(); diff --git a/src/commands/moulberry-bush/capePerms.ts b/src/commands/moulberry-bush/capePerms.ts index 3e4563a..d1850c8 100644 --- a/src/commands/moulberry-bush/capePerms.ts +++ b/src/commands/moulberry-bush/capePerms.ts @@ -1,17 +1,8 @@ -import { Message, MessageEmbed } from 'discord.js'; +import { Constants } from 'discord-akairo'; +import { MessageEmbed } from 'discord.js'; import got from 'got'; -import { SlashCommandOption } from '../../lib/extensions/BushClientUtil'; import { BushCommand } from '../../lib/extensions/BushCommand'; -import { BushSlashMessage } from '../../lib/extensions/BushInteractionMessage'; - -interface Capeperms { - success: boolean; - perms: User[]; -} -interface User { - _id: string; - perms: string[]; -} +import { BushMessage } from '../../lib/extensions/BushMessage'; export default class CapePermissionsCommand extends BushCommand { private nameMap = { @@ -19,31 +10,32 @@ export default class CapePermissionsCommand extends BushCommand { patreon2: 'Patreon Tier 2', fade: 'Fade', contrib: 'Contributor', - nullzee: 'Nullzee', - gravy: 'ThatGravyBoat', - space: 'Space', - mcworld: 'Minecraft World', - lava: 'Lava', - packshq: 'PacksHQ', - mbstaff: "Moulberry's Bush staff", - thebakery: "Biscuit's Bakery", - negative: 'Negative', - void: 'Void', - ironmoon: 'IRONM00N', - krusty: 'Krusty', - furf: 'FurfSky Reborn', - soldier: 'Soldier', - dsm: "Danker's Skyblock Mod", - zera: 'Zera', - tunnel: 'Tunnel', - alexxoffi: 'Alexxoffi', - parallax: 'Parallax', - jakethybro: 'Jakethybro', - planets: 'Planets' + nullzee: 'Patreon Tier 1', + gravy: 'Patreon Tier 1', + space: 'Patreon Tier 1', + mcworld: 'Patreon Tier 1', + lava: 'Patreon Tier 1', + packshq: 'Patreon Tier 1', + mbstaff: 'Patreon Tier 1', + thebakery: 'Patreon Tier 1', + negative: 'Patreon Tier 1', + void: 'Patreon Tier 1', + ironmoon: 'Patreon Tier 1', + krusty: 'Patreon Tier 1', + furf: 'Patreon Tier 1', + soldier: 'Patreon Tier 1', + dsm: 'Patreon Tier 1', + zera: 'Patreon Tier 1', + tunnel: 'Patreon Tier 1', + alexxoffi: 'Patreon Tier 1', + parallax: 'Patreon Tier 1', + jakethybro: 'Patreon Tier 1', + planets: 'Patreon Tier 1' }; + public constructor() { - super('capeperms', { - aliases: ['capeperms', 'capeperm', 'capepermissions', 'capepermission'], + super('capepermissions', { + aliases: ['capeperms', 'capeperm', 'capepermissions'], category: "Moulberry's Bush", description: { content: 'A command to see what capes someone has access to.', @@ -52,8 +44,9 @@ export default class CapePermissionsCommand extends BushCommand { }, args: [ { - id: 'user', - type: 'string', + id: 'ign', + type: Constants.ArgumentTypes.STRING, + match: Constants.ArgumentMatches.PHRASE, prompt: { start: 'Who would you like to see the cape permissions of?', retry: '{error} Choose someone to see the capes their available capes.', @@ -63,53 +56,72 @@ export default class CapePermissionsCommand extends BushCommand { ], clientPermissions: ['EMBED_LINKS', 'SEND_MESSAGES'], channel: 'guild', + slash: true, slashOptions: [ { + name: 'ign', + description: 'The ign of the player you would like to view the capes permissions of.', type: 'STRING', - name: 'user', - description: 'The username of the player to see the cape permissions of', required: true } - ], - slash: true + ] }); } - private async getResponse(user: string): Promise<{ content?: string; embeds?: MessageEmbed[] }> { + + public async exec(message: BushMessage, args: { ign: string }): Promise<unknown> { + interface Capeperms { + success: boolean; + perms: User[]; + } + + interface User { + _id: string; + perms: string[]; + } + let capeperms: Capeperms, uuid: string; try { - uuid = await this.client.util.mcUUID(user); + uuid = await this.client.util.mcUUID(args.ign); } catch (e) { - return { content: `${this.client.util.emojis.error} \`${user}\` doesn't appear to be a valid username.` }; + return await message.util.reply( + `${this.client.util.emojis.error} \`${args.ign}\` doesn't appear to be a valid username.` + ); } try { - capeperms = await got.get('http://moulberry.codes/permscapes.json').json(); + capeperms = await got.get('https://moulberry.codes/permscapes.json').json(); } catch (error) { capeperms = null; } if (capeperms == null) { - return { content: `${this.client.util.emojis.error} There was an error finding cape perms for \`${user}\`.` }; + return await message.util.reply( + `${this.client.util.emojis.error} There was an error finding cape perms for \`${args.ign}\`.` + ); } else { if (capeperms?.perms) { - const foundUser = capeperms.perms.find((u) => u._id === uuid); - if (foundUser == null) - return { content: `${this.client.util.emojis.error} \`${user}\` does not appear to have any capes.` }; - const userPerm: string[] = foundUser.perms; - const embed = this.client.util - .createEmbed(this.client.util.colors.default) - .setTitle(`${user}'s Capes`) - .setDescription(userPerm.join('\n')); - return { embeds: [embed] }; + let index = null; + + for (let i = 0; i < capeperms.perms.length; i++) { + if (capeperms.perms[i]._id == uuid) { + index = i; + break; + } + } + if (index == null) + return await message.util.reply( + `${this.client.util.emojis.error} \`${args.ign}\` does not appear to have any capes.` + ); + const userPerm: string[] = capeperms.perms[index].perms; + const embed = new MessageEmbed() + .setTitle(`${args.ign}'s Capes`) + .setDescription(userPerm.join('\n')) + .setColor(this.client.util.colors.default); + await message.util.reply({ embeds: [embed] }); } else { - return { content: `${this.client.util.emojis.error} There was an error finding cape perms for ${user}.` }; + return await message.util.reply( + `${this.client.util.emojis.error} There was an error finding cape perms for ${args.ign}.` + ); } } } - public async exec(message: Message, { user }: { user: string }): Promise<void> { - await message.reply(await this.getResponse(user)); - } - - public async execSlash(message: BushSlashMessage, { user }: { user: SlashCommandOption<string> }): Promise<void> { - await message.reply(await this.getResponse(user.value)); - } } diff --git a/src/commands/moulberry-bush/giveawayPing.ts b/src/commands/moulberry-bush/giveawayPing.ts index d308602..19163d7 100644 --- a/src/commands/moulberry-bush/giveawayPing.ts +++ b/src/commands/moulberry-bush/giveawayPing.ts @@ -1,5 +1,5 @@ -import { Message, NewsChannel, TextChannel, WebhookClient } from 'discord.js'; import { BushCommand } from '../../lib/extensions/BushCommand'; +import { BushMessage } from '../../lib/extensions/BushMessage'; import AllowedMentions from '../../lib/utils/AllowedMentions'; export default class GiveawayPingCommand extends BushCommand { @@ -19,16 +19,26 @@ export default class GiveawayPingCommand extends BushCommand { ignorePermissions: [], cooldown: 1.44e7, //4 hours ratelimit: 1, - editable: false + editable: false, + restrictedGuilds: ['516977525906341928'], + restrictedChannels: ['767782084981817344', '833855738501267456'] }); } - public async exec(message: Message): Promise<unknown> { - if (message.guild.id !== '516977525906341928') - return message.reply(`${this.client.util.emojis.error} This command may only be run in Moulberry's Bush.`); - if (!['767782084981817344', '833855738501267456'].includes(message.channel.id)) - return message.reply(`${this.client.util.emojis.error} This command may only be run in giveaway channels.`); - await message.delete().catch(() => undefined); - const webhooks = await (message.channel as TextChannel | NewsChannel).fetchWebhooks(); + + public async exec(message: BushMessage): Promise<unknown> { + if (!message.member.permissions.has('MANAGE_GUILD')) + await message.util.reply(`${this.client.util.emojis.error} You are missing the \`manage server\` permission.`); + + await message.delete().catch(() => {}); + + return await message.channel.send({ + content: + '🎉 <@&767782793261875210> Giveaway.\n\n<:mad:783046135392239626> Spamming, line breaking, gibberish etc. disqualifies you from winning. We can and will ban you from giveaways. Winners will all be checked and rerolled if needed.', + allowedMentions: AllowedMentions.roles() + }); + + //! Broken + /* const webhooks = await (message.channel as TextChannel | NewsChannel).fetchWebhooks(); let webhookClient: WebhookClient; if (webhooks.size < 1) { const webhook = await (message.channel as TextChannel | NewsChannel).createWebhook('Giveaway ping webhook'); @@ -37,11 +47,12 @@ export default class GiveawayPingCommand extends BushCommand { const webhook = webhooks.first(); webhookClient = new WebhookClient(webhook.id, webhook.token); } - return webhookClient.send({ - content: `🎉 <@&767782793261875210> Giveaway.\n\n${this.client.util.emojis.mad} Spamming, line breaking, gibberish etc. disqualifies you from winning. We can and will ban you from giveaways. Winners will all be checked and rerolled if needed.`, + return await webhookClient.send({ + content: + '🎉 <@&767782793261875210> Giveaway.\n\n<:mad:783046135392239626> Spamming, line breaking, gibberish etc. disqualifies you from winning. We can and will ban you from giveaways. Winners will all be checked and rerolled if needed.', username: `${message.member.nickname || message.author.username}`, avatarURL: message.author.avatarURL({ dynamic: true }), allowedMentions: AllowedMentions.roles() - }); + }); */ } } diff --git a/src/commands/moulberry-bush/level.ts b/src/commands/moulberry-bush/level.ts index f822555..41541e0 100644 --- a/src/commands/moulberry-bush/level.ts +++ b/src/commands/moulberry-bush/level.ts @@ -1,4 +1,4 @@ -import { CommandInteractionOption, Message, User } from 'discord.js'; +import { Message, User } from 'discord.js'; import { BushCommand } from '../../lib/extensions/BushCommand'; import { BushSlashMessage } from '../../lib/extensions/BushInteractionMessage'; import { Level } from '../../lib/models'; @@ -26,7 +26,7 @@ export default class LevelCommand extends BushCommand { type: 'user', prompt: { start: 'What user would you like to see the level of?', - retry: 'Invalid user. What user would you like to see the level of?', + retry: '{error} Choose a valid user to see the level of.', optional: true } } @@ -139,7 +139,7 @@ export default class LevelCommand extends BushCommand { } } - async exec(message: Message, { user }: { user?: User }): Promise<void> { + async exec(message: Message | BushSlashMessage, { user }: { user?: User }): Promise<void> { // await message.reply( // new MessageAttachment( // await this.getImage(user || message.author), @@ -148,13 +148,4 @@ export default class LevelCommand extends BushCommand { // ); await message.reply(await this.getResponse(user || message.author)); } - async execSlash(message: BushSlashMessage, { user }: { user?: CommandInteractionOption }): Promise<void> { - // await message.reply( - // new MessageAttachment( - // await this.getImage(user?.user || message.user), - // 'lel.png' - // ) - // ); - await message.reply(await this.getResponse(user?.user || message.author)); - } } diff --git a/src/commands/moulberry-bush/rule.ts b/src/commands/moulberry-bush/rule.ts index e9b09eb..3cd9ec6 100644 --- a/src/commands/moulberry-bush/rule.ts +++ b/src/commands/moulberry-bush/rule.ts @@ -1,69 +1,60 @@ -import { Argument } from 'discord-akairo'; -import { CommandInteraction, Message, MessageEmbed, User } from 'discord.js'; -import { SlashCommandOption } from '../../lib/extensions/BushClientUtil'; +import { Argument, Constants } from 'discord-akairo'; +import { MessageEmbed, User } from 'discord.js'; import { BushCommand } from '../../lib/extensions/BushCommand'; -import { BushSlashMessage } from '../../lib/extensions/BushInteractionMessage'; +import { BushMessage } from '../../lib/extensions/BushMessage'; +import AllowedMentions from '../../lib/utils/AllowedMentions'; +const rules = [ + { + title: "1.) Follow Discord's TOS", + description: + "Be sure to follow discord's TOS found at <https://discordapp.com/tos>, you must be 13 to use discord so if you admit to being under 13 you will be banned from the server." + }, + { + title: '2.) Be Respectful', + description: + 'Racist, sexist, homophobic, xenophobic, transphobic, ableist, hate speech, slurs, or any other derogatory, toxic, or discriminatory behavior will not be tolerated.' + }, + { + title: '3.) No Spamming', + description: + 'Including but not limited to: any messages that do not contribute to the conversation, repeated messages, randomly tagging users, and chat flood.' + }, + { + title: '4.) English', + description: 'The primary language of the server is English, please keep all discussions in English.' + }, + { + title: '5.) Safe for Work', + description: + 'Please keep NSFW and NSFL content out of this server, avoid borderline images as well as keeping your status, profile picture, and banner SFW.' + }, + { + title: '6.) No Advertising', + description: 'Do not promote anything without prior approval from a staff member, this includes DM advertising.' + }, + { + title: '7.) Impersonation', + description: + 'Do not try to impersonate others for the express intent of being deceitful, defamation , and/or personal gain.' + }, + { title: '8.) Swearing', description: 'Swearing is allowed only when not used as an insult.' }, + { + title: "9.) Sending media that are able to crash a user's Discord", + description: + "Sending videos, GIFs, emojis, etc. that are able to crash someone's discord will result in a **permanent** ban that cannot be appealed." + }, + { + title: '10.) No Backseat Moderating', + description: 'If you see a rule being broken be broken, please report it using: `-report <user> [evidence]`.' + }, + { + title: '11.) Staff may moderate at their discretion', + description: + 'If there are loopholes in our rules, the staff team may moderate based on what they deem appropriate. The staff team holds final discretion.' + } +]; export default class RuleCommand extends BushCommand { - private rules = [ - { - title: "Follow Discord's TOS", - description: - "Be sure to follow discord's TOS found at <https://discordapp.com/tos>, you must be 13 to use discord so if you admit to being under 13 you will be banned from the server." - }, - { - title: 'Be Respectful', - description: - 'Racist, sexist, homophobic, xenophobic, transphobic, ableist, hate speech, slurs, or any other derogatory, toxic, or discriminatory behavior will not be tolerated.' - }, - { - title: 'No Spamming', - description: - 'Including but not limited to: any messages that do not contribute to the conversation, repeated messages, randomly tagging users, and chat flood.' - }, - { - title: 'English', - description: 'The primary language of the server is English, please keep all discussions in English.' - }, - { - title: 'Safe for Work', - description: - 'Please keep NSFW and NSFL content out of this server, avoid borderline images as well as keeping your status and profile picture SFW.' - }, - { - title: 'No Advertising', - description: 'Do not promote anything without prior approval from a staff member, this includes DM advertising.' - }, - { - title: 'Impersonation', - description: - 'Do not try to impersonate others for the express intent of being deceitful, defamation , and/or personal gain.' - }, - { - title: 'Swearing', - description: 'Swearing is allowed only when not used as an insult.' - }, - { - title: 'Only ping @emergency in emergencies', - description: - 'Pinging <@&833802660209229854> for no reason will result in severe punishment. <@&833802660209229854> is only to be pinged in true emergencies.' - }, - { - title: 'No Backseat Moderating', - description: 'If you see a rule being broken be broken, please report it using: `-report <user> [evidence]`.' - }, - { - title: 'Staff may moderate at their discretion', - description: - 'If there are loopholes in our rules, the staff team may moderate based on what they deem appropriate. The staff team holds final discretion.' - }, - { - title: "Sending media that are able to crash a user's Discord", - description: - "Sending videos, GIFs, emojis, etc. that are able to crash someone's discord will result in a **permanent** mute that cannot be appealed." - } - ]; - public constructor() { super('rule', { aliases: ['rule', 'rules'], @@ -76,98 +67,93 @@ export default class RuleCommand extends BushCommand { args: [ { id: 'rule', - type: Argument.range('number', 1, 12, true), + type: Argument.range(Constants.ArgumentTypes.INTEGER, 1, rules.length, true), + match: Constants.ArgumentMatches.PHRASE, prompt: { start: 'What rule would you like to have cited?', retry: '{error} Choose a valid rule.', optional: true - }, - default: undefined + } }, { id: 'user', type: 'user', + match: Constants.ArgumentMatches.PHRASE, prompt: { start: 'What user would you like to mention?', retry: '{error} Choose a valid user to mention.', optional: true - }, - default: undefined + } } ], clientPermissions: ['EMBED_LINKS', 'SEND_MESSAGES'], channel: 'guild', + restrictedGuilds: ['516977525906341928'], + slash: true, slashOptions: [ { - type: 'INTEGER', name: 'rule', - description: 'The rule to show', + description: 'The rule you would you like to have cited', + type: 'INTEGER', required: false }, { - type: 'USER', name: 'user', - description: 'The user to ping', + description: 'The user you would like to mention.', + type: 'USER', required: false } ], - slash: true + slashGuilds: ['516977525906341928'] }); } - private getResponse( - message: Message | CommandInteraction, - rule?: number, - user?: User - ): { content?: string; embeds?: MessageEmbed[] } | [string, MessageEmbed] { - if ( - message.guild.id !== '516977525906341928' && - !this.client.ownerID.includes(message instanceof Message ? message.author.id : message.user.id) - ) { - return { content: `${this.client.util.emojis.error} This command can only be run in Moulberry's Bush.` }; - } - let rulesEmbed = new MessageEmbed().setColor('ef3929'); - if (message instanceof Message) { - rulesEmbed = rulesEmbed.setFooter(`Triggered by ${message.author.tag}`, message.author.avatarURL({ dynamic: true })); + + public async exec(message: BushMessage, { rule, user }: { rule: undefined | number; user: User }): Promise<unknown> { + const rulesEmbed = new MessageEmbed() + .setColor('ef3929') + .setFooter(`Triggered by ${message.author.tag}`, message.author.avatarURL({ dynamic: true })) + .setTimestamp(); + + if (rule > 12 || rule < 1) { + rule = undefined; } if (rule) { - const foundRule = this.rules[rule - 1]; - rulesEmbed.addField(`${rule}) ${foundRule.title}`, foundRule.description); + if (rules[rule - 1]?.title && rules[rule - 1]?.description) + rulesEmbed.addField(rules[rule - 1].title, rules[rule - 1].description); } else { - for (const curRule of this.rules) { - rulesEmbed.addField(`${this.rules.indexOf(curRule) + 1}) ${curRule.title}`, curRule.description); + for (let i = 0; i < rules.length; i++) { + if (rules[i]?.title && rules[i]?.description) rulesEmbed.addField(rules[i].title, rules[i].description); } } - if (!user) { - return { embeds: [rulesEmbed] }; - } else { - return [`<@!${user.id}>`, rulesEmbed]; - } - } - public async exec(message: Message, { rule, user }: { rule?: number; user?: User }): Promise<void> { - const response = this.getResponse(message, rule, user); - if (Array.isArray(response)) { - await message.util.send({ - content: response[0], - embeds: [response[1]] - }); - } else { - await message.util.send(response); + await respond(); + if (!message.util.isSlash) { + await message.delete().catch(() => {}); } - await message.delete().catch(() => undefined); - } - - public async execSlash( - message: BushSlashMessage, - { rule, user }: { rule?: SlashCommandOption<number>; user?: SlashCommandOption<void> } - ): Promise<void> { - const response = this.getResponse(message.interaction, rule?.value, user?.user); - if (Array.isArray(response)) { - await message.interaction.reply({ - content: response[0], - embeds: [response[1]] - }); - } else { - await message.interaction.reply(response); + return; + async function respond(): Promise<unknown> { + if (!user) { + return ( + // 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.util.send({ embeds: [rulesEmbed], allowedMentions: AllowedMentions.users() }) + ); + } else { + return message.reference?.messageID && !message.util.isSlash + ? await message.util.send({ + content: `<@!${user.id}>`, + embeds: [rulesEmbed], + allowedMentions: AllowedMentions.users(), + reply: { messageReference: message.reference.messageID } + }) + : await message.util.send({ + content: `<@!${user.id}>`, + embeds: [rulesEmbed], + allowedMentions: AllowedMentions.users() + }); + } } } } diff --git a/src/inhibitors/blacklist/blacklist.ts b/src/inhibitors/blacklist/blacklist.ts deleted file mode 100644 index 309815f..0000000 --- a/src/inhibitors/blacklist/blacklist.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { BushInhibitor } from '../../lib/extensions/BushInhibitor'; - -export default class BlacklistInhibitor extends BushInhibitor { - constructor() { - super('blacklist', { - reason: 'blacklist' - }); - } - - public exec(): boolean | Promise<boolean> { - // This is just a placeholder for now - return false; - } -} diff --git a/src/inhibitors/blacklist/guildBlacklist.ts b/src/inhibitors/blacklist/guildBlacklist.ts new file mode 100644 index 0000000..93d8aee --- /dev/null +++ b/src/inhibitors/blacklist/guildBlacklist.ts @@ -0,0 +1,19 @@ +import { BushInhibitor } from '../../lib/extensions/BushInhibitor'; +import { BushSlashMessage } from '../../lib/extensions/BushInteractionMessage'; +import { BushMessage } from '../../lib/extensions/BushMessage'; + +export default class GuildBlacklistInhibitor extends BushInhibitor { + constructor() { + super('guildBlacklist', { + reason: 'guildBlacklist', + category: 'blacklist', + type: 'all' + }); + } + + public exec(message: BushMessage | BushSlashMessage): boolean { + if (!message.guild) return false; + if (message.author && (this.client.isOwner(message.author) || this.client.isSuperUser(message.author))) return false; + return this.client.cache.global.blacklistedGuilds.includes(message.guild.id); + } +} diff --git a/src/inhibitors/blacklist/userBlacklist.ts b/src/inhibitors/blacklist/userBlacklist.ts new file mode 100644 index 0000000..bbced28 --- /dev/null +++ b/src/inhibitors/blacklist/userBlacklist.ts @@ -0,0 +1,19 @@ +import { BushInhibitor } from '../../lib/extensions/BushInhibitor'; +import { BushSlashMessage } from '../../lib/extensions/BushInteractionMessage'; +import { BushMessage } from '../../lib/extensions/BushMessage'; + +export default class UserBlacklistInhibitor extends BushInhibitor { + constructor() { + super('userBlacklist', { + reason: 'userBlacklist', + category: 'blacklist', + type: 'all' + }); + } + + public exec(message: BushMessage | BushSlashMessage): boolean { + if (!message.author) return false; + if (this.client.isOwner(message.author) || this.client.isSuperUser(message.author)) return false; + return this.client.cache.global.blacklistedUsers.includes(message.author.id); + } +} diff --git a/src/inhibitors/commands/disabledCommand.ts b/src/inhibitors/commands/disabledCommand.ts new file mode 100644 index 0000000..8538858 --- /dev/null +++ b/src/inhibitors/commands/disabledCommand.ts @@ -0,0 +1,19 @@ +import { BushCommand } from '../../lib/extensions/BushCommand'; +import { BushInhibitor } from '../../lib/extensions/BushInhibitor'; +import { BushSlashMessage } from '../../lib/extensions/BushInteractionMessage'; +import { BushMessage } from '../../lib/extensions/BushMessage'; + +export default class DisabledCommandInhibitor extends BushInhibitor { + constructor() { + super('disabledCommand', { + reason: 'disabled', + type: 'pre', + priority: 3 + }); + } + + public async exec(message: BushMessage | BushSlashMessage, command: BushCommand): Promise<boolean> { + if (this.client.isOwner(message.author)) return false; + return this.client.cache.global.disabledCommands.includes(command?.id); + } +} diff --git a/src/inhibitors/noCache.ts b/src/inhibitors/noCache.ts new file mode 100644 index 0000000..61f0b3e --- /dev/null +++ b/src/inhibitors/noCache.ts @@ -0,0 +1,21 @@ +import { BushInhibitor } from '../lib/extensions/BushInhibitor'; +import { BushSlashMessage } from '../lib/extensions/BushInteractionMessage'; +import { BushMessage } from '../lib/extensions/BushMessage'; + +export default class noCacheInhibitor extends BushInhibitor { + constructor() { + super('noCache', { + reason: 'noCache', + type: 'all', + priority: 100 + }); + } + + public async exec(message: BushMessage | BushSlashMessage): Promise<boolean> { + if (this.client.isOwner(message.author)) return false; + for (const property in this.client.cache) { + if (property === undefined || property === null) return true; + } + return false; + } +} diff --git a/src/lib/extensions/BushArgumentOptions.ts b/src/lib/extensions/BushArgumentOptions.ts new file mode 100644 index 0000000..bbbc04b --- /dev/null +++ b/src/lib/extensions/BushArgumentOptions.ts @@ -0,0 +1,59 @@ +import { ArgumentOptions, ArgumentTypeCaster } from 'discord-akairo'; + +type BushArgumentType = + | 'string' + | 'lowercase' + | 'uppercase' + | 'charCodes' + | 'number' + | 'integer' + | 'bigint' + | 'emojint' + | 'url' + | 'date' + | 'color' + | 'user' + | 'users' + | 'member' + | 'members' + | 'relevant' + | 'relevants' + | 'channel' + | 'channels' + | 'textChannel' + | 'textChannels' + | 'voiceChannel' + | 'voiceChannels' + | 'categoryChannel' + | 'categoryChannels' + | 'newsChannel' + | 'newsChannels' + | 'storeChannel' + | 'storeChannels' + | 'role' + | 'roles' + | 'emoji' + | 'emojis' + | 'guild' + | 'guilds' + | 'message' + | 'guildMessage' + | 'relevantMessage' + | 'invite' + | 'userMention' + | 'memberMention' + | 'channelMention' + | 'roleMention' + | 'emojiMention' + | 'commandAlias' + | 'command' + | 'inhibitor' + | 'listener' + | 'duration' + | (string | string[])[] + | RegExp + | string; + +export interface BushArgumentOptions extends ArgumentOptions { + type?: BushArgumentType | ArgumentTypeCaster; +} diff --git a/src/lib/extensions/BushArgumentTypeCaster.ts b/src/lib/extensions/BushArgumentTypeCaster.ts new file mode 100644 index 0000000..e000063 --- /dev/null +++ b/src/lib/extensions/BushArgumentTypeCaster.ts @@ -0,0 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { BushMessage } from './BushMessage'; + +export type BushArgumentTypeCaster = (message: BushMessage, phrase: string) => any; diff --git a/src/lib/extensions/BushClient.ts b/src/lib/extensions/BushClient.ts index b12fd52..73b0864 100644 --- a/src/lib/extensions/BushClient.ts +++ b/src/lib/extensions/BushClient.ts @@ -1,11 +1,13 @@ import chalk from 'chalk'; -import { AkairoClient, TaskHandler } from 'discord-akairo'; +import { AkairoClient } from 'discord-akairo'; import { APIMessage, Guild, Intents, Message, MessageOptions, Snowflake, UserResolvable } from 'discord.js'; import * as path from 'path'; import { exit } from 'process'; import readline from 'readline'; import { Sequelize } from 'sequelize'; +import { durationTypeCaster } from '../../arguments/duration'; import * as config from '../../config/options'; +import UpdateCacheTask from '../../tasks/updateCache'; import * as Models from '../models'; import AllowedMentions from '../utils/AllowedMentions'; import { BushCache } from '../utils/BushCache'; @@ -15,6 +17,7 @@ import { BushClientUtil } from './BushClientUtil'; import { BushCommandHandler } from './BushCommandHandler'; import { BushInhibitorHandler } from './BushInhinitorHandler'; import { BushListenerHandler } from './BushListenerHandler'; +import { BushTaskHandler } from './BushTaskHandler'; export type BotConfig = typeof config; export type BushMessageType = string | APIMessage | (MessageOptions & { split?: false }); @@ -30,7 +33,7 @@ export class BushClient extends AkairoClient { public listenerHandler: BushListenerHandler; public inhibitorHandler: BushInhibitorHandler; public commandHandler: BushCommandHandler; - public taskHandler: TaskHandler; + public taskHandler: BushTaskHandler; public declare util: BushClientUtil; public declare ownerID: Snowflake[]; public db: Sequelize; @@ -68,7 +71,7 @@ export class BushClient extends AkairoClient { }); // Create task handler - this.taskHandler = new TaskHandler(this, { + this.taskHandler = new BushTaskHandler(this, { directory: path.join(__dirname, '..', '..', 'tasks') }); @@ -76,14 +79,14 @@ export class BushClient extends AkairoClient { this.commandHandler = new BushCommandHandler(this, { directory: path.join(__dirname, '..', '..', 'commands'), prefix: async ({ guild }: { guild: Guild }) => { - if (this.config.dev) return 'dev'; + if (this.config.dev) return 'dev '; const row = await Models.Guild.findByPk(guild.id); return (row?.prefix || this.config.prefix) as string; }, allowMention: true, handleEdits: true, commandUtil: true, - commandUtilLifetime: 3e5, + commandUtilLifetime: 300_000, argumentDefaults: { prompt: { start: 'Placeholder argument prompt. If you see this please tell the devs.', @@ -99,9 +102,8 @@ export class BushClient extends AkairoClient { }, otherwise: '' }, - ignorePermissions: this.config.owners, - ignoreCooldown: this.config.owners, - automateCategories: true, + + automateCategories: false, autoRegisterSlashCommands: true }); @@ -110,7 +112,7 @@ export class BushClient extends AkairoClient { dialect: 'postgres', host: this.config.db.host, port: this.config.db.port, - logging: this.config.logging ? (a) => this.logger.debug(a) : false + logging: this.config.logging.db ? (a) => this.logger.debug(a) : false }); this.logger = new BushLogger(this); } @@ -127,6 +129,8 @@ export class BushClient extends AkairoClient { private async _init(): Promise<void> { this.commandHandler.useListenerHandler(this.listenerHandler); this.commandHandler.useInhibitorHandler(this.inhibitorHandler); + this.commandHandler.ignorePermissions = this.config.owners; + this.commandHandler.ignoreCooldown = this.config.owners.concat(this.cache.global.superUsers); this.listenerHandler.setEmitters({ client: this, commandHandler: this.commandHandler, @@ -137,6 +141,9 @@ export class BushClient extends AkairoClient { stdin: rl, gateway: this.ws }); + this.commandHandler.resolver.addTypes({ + duration: durationTypeCaster + }); // loads all the handlers const loaders = { commands: this.commandHandler, @@ -147,13 +154,15 @@ export class BushClient extends AkairoClient { for (const loader of Object.keys(loaders)) { try { loaders[loader].loadAll(); - this.logger.success('Startup', `Successfully loaded <<${loader}>>.`, false); + await this.logger.success('Startup', `Successfully loaded <<${loader}>>.`, false); } catch (e) { - this.logger.error('Startup', `Unable to load loader <<${loader}>> with error:\n${e?.stack}`, false); + await this.logger.error('Startup', `Unable to load loader <<${loader}>> with error:\n${e?.stack}`, false); } } - this.taskHandler.startAll(); await this.dbPreInit(); + await new UpdateCacheTask().init(this); + this.console.success('Startup', `Successfully created <<global cache>>.`, false); + this.taskHandler.startAll(); } public async dbPreInit(): Promise<void> { @@ -161,14 +170,15 @@ export class BushClient extends AkairoClient { await this.db.authenticate(); Models.Global.initModel(this.db); Models.Guild.initModel(this.db, this); - Models.Modlog.initModel(this.db); + Models.ModLog.initModel(this.db); Models.Ban.initModel(this.db); + Models.Mute.initModel(this.db); Models.Level.initModel(this.db); Models.StickyRole.initModel(this.db); await this.db.sync({ alter: true }); // Sync all tables to fix everything if updated - this.console.success('Startup', `Successfully connected to <<database>>.`, false); + await this.console.success('Startup', `Successfully connected to <<database>>.`, false); } catch (error) { - this.console.error('Startup', `Failed to connect to <<database>> with error:\n` + error?.stack, false); + await this.console.error('Startup', `Failed to connect to <<database>> with error:\n` + error?.stack, false); } } @@ -178,7 +188,7 @@ export class BushClient extends AkairoClient { await this._init(); await this.login(this.token); } catch (e) { - this.console.error('Start', chalk.red(e.stack), false); + await this.console.error('Start', chalk.red(e.stack), false); exit(2); } } @@ -196,6 +206,6 @@ export class BushClient extends AkairoClient { } public isSuperUser(user: UserResolvable): boolean { const userID = this.users.resolveID(user); - return !!BushCache?.superUsers?.includes(userID) || this.config.owners.includes(userID); + return !!BushCache?.global?.superUsers?.includes(userID) || this.config.owners.includes(userID); } } diff --git a/src/lib/extensions/BushClientUtil.ts b/src/lib/extensions/BushClientUtil.ts index a6b049a..34a9e83 100644 --- a/src/lib/extensions/BushClientUtil.ts +++ b/src/lib/extensions/BushClientUtil.ts @@ -2,19 +2,11 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { exec } from 'child_process'; import { ClientUtil } from 'discord-akairo'; -import { - APIInteractionDataResolvedChannel, - APIInteractionDataResolvedGuildMember, - APIMessage, - APIRole, - ApplicationCommandOptionType -} from 'discord-api-types'; +import { APIMessage } from 'discord-api-types'; import { ButtonInteraction, CommandInteraction, - CommandInteractionOption, Constants, - GuildChannel, GuildMember, InteractionReplyOptions, Message, @@ -24,7 +16,6 @@ import { MessageEditOptions, MessageEmbed, MessageOptions, - Role, Snowflake, TextChannel, User, @@ -33,7 +24,7 @@ import { } from 'discord.js'; import got from 'got'; import { promisify } from 'util'; -import { Global } from '../models/Global'; +import { Global } from '../models'; import { BushCache } from '../utils/BushCache'; import { BushClient } from './BushClient'; import { BushMessage } from './BushMessage'; @@ -61,17 +52,6 @@ export interface uuidRes { created_at: string; } -export interface SlashCommandOption<T> { - name: string; - type: ApplicationCommandOptionType; - value?: T; - options?: CommandInteractionOption[]; - user?: User; - member?: GuildMember | APIInteractionDataResolvedGuildMember; - channel?: GuildChannel | APIInteractionDataResolvedChannel; - role?: Role | APIRole; -} - export class BushClientUtil extends ClientUtil { /** The client of this ClientUtil */ public declare client: BushClient; @@ -147,7 +127,6 @@ export class BushClientUtil extends ClientUtil { return `${url}/${res.key}`; } catch (e) { this.client.console.error('Haste', `Unable to upload haste to ${url}`); - continue; } } return 'Unable to post'; @@ -163,8 +142,7 @@ export class BushClientUtil extends ClientUtil { const idMatch = text.match(idReg); if (idMatch) { try { - const user = await this.client.users.fetch(text as Snowflake); - return user; + return await this.client.users.fetch(text as Snowflake); } catch { // pass } @@ -173,8 +151,7 @@ export class BushClientUtil extends ClientUtil { const mentionMatch = text.match(mentionReg); if (mentionMatch) { try { - const user = await this.client.users.fetch(mentionMatch.groups.id as Snowflake); - return user; + return await this.client.users.fetch(mentionMatch.groups.id as Snowflake); } catch { // pass } @@ -460,8 +437,8 @@ export class BushClientUtil extends ClientUtil { } /** Gets the channel configs as a TextChannel */ - public getConfigChannel(channel: 'log' | 'error' | 'dm'): Promise<TextChannel> { - return this.client.channels.fetch(this.client.config.channels[channel]) as Promise<TextChannel>; + public async getConfigChannel(channel: 'log' | 'error' | 'dm'): Promise<TextChannel> { + return (await this.client.channels.fetch(this.client.config.channels[channel])) as TextChannel; } /** @@ -488,7 +465,7 @@ export class BushClientUtil extends ClientUtil { public async insertOrRemoveFromGlobal( action: 'add' | 'remove', - key: keyof typeof BushCache, + key: keyof typeof BushCache['global'], value: any ): Promise<Global | void> { const environment = this.client.config.dev ? 'development' : 'production'; @@ -502,7 +479,34 @@ export class BushClientUtil extends ClientUtil { newValue = oldValue.filter((ae) => ae !== value); } row[key] = newValue; - this.client.cache[key] = newValue; + this.client.cache.global[key] = newValue; return await row.save().catch((e) => this.client.logger.error('insertOrRemoveFromGlobal', e)); } + + /** + * Surrounds a string to the begging an end of each element in an array. + * + * @param {string[]} array The array you want to surround. + * @param {string} surroundChar1 The character placed in the beginning of the element (or end if surroundChar2 isn't supplied). + * @param {string} [surroundChar2=surroundChar1] The character placed in the end of the element. + * @returns {string[]} + */ + public surroundArray(array: string[], surroundChar1: string, surroundChar2?: string): string[] { + const newArray = []; + array.forEach((a) => { + newArray.push(`${surroundChar1}${a}${surroundChar2 || surroundChar1}`); + }); + return newArray; + } + + // public createModLogEntry( + // user: User | Snowflake, + // guild: Guild | Snowflake, + // reason?: string, + // type?: ModLogType, + // duration?: number, + // moderator: User | Snowflake + // ): ModLog { + + // } } diff --git a/src/lib/extensions/BushCommand.ts b/src/lib/extensions/BushCommand.ts index bc6ff68..b62d26e 100644 --- a/src/lib/extensions/BushCommand.ts +++ b/src/lib/extensions/BushCommand.ts @@ -1,33 +1,47 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Command, CommandOptions } from 'discord-akairo'; +import { ArgumentGenerator, ArgumentOptions, ArgumentPromptOptions, Command, CommandOptions } from 'discord-akairo'; import { Snowflake } from 'discord.js'; import { BushClient } from './BushClient'; import { BushCommandHandler } from './BushCommandHandler'; import { BushSlashMessage } from './BushInteractionMessage'; import { BushMessage } from './BushMessage'; +export interface BushArgumentOptions extends ArgumentOptions { + id: string; + description?: string; + prompt?: ArgumentPromptOptions; +} + export interface BushCommandOptions extends CommandOptions { hidden?: boolean; restrictedChannels?: Snowflake[]; restrictedGuilds?: Snowflake[]; description: { content: string; - usage: string; - examples: string[]; + usage: string | string[]; + examples: string | string[]; }; + args?: BushArgumentOptions[] | ArgumentGenerator; + category: string; } export class BushCommand extends Command { public declare client: BushClient; + public declare handler: BushCommandHandler; + public options: BushCommandOptions; + /** The channels the command is limited to run in. */ public restrictedChannels: Snowflake[]; + /** The guilds the command is limited to run in. */ public restrictedGuilds: Snowflake[]; + /** Whether the command is hidden from the help command. */ public hidden: boolean; + constructor(id: string, options?: BushCommandOptions) { super(id, options); this.options = options; diff --git a/src/lib/extensions/BushCommandHandler.ts b/src/lib/extensions/BushCommandHandler.ts index 8e8936e..aeea101 100644 --- a/src/lib/extensions/BushCommandHandler.ts +++ b/src/lib/extensions/BushCommandHandler.ts @@ -86,9 +86,6 @@ export class BushCommandHandler extends CommandHandler { this.emit(CommandHandlerEvents.COMMAND_BLOCKED, message, command, reason); return true; } - if (this.runCooldowns(message, command)) { - return true; - } - return false; + return !!this.runCooldowns(message, command); } } diff --git a/src/lib/extensions/BushInhibitor.ts b/src/lib/extensions/BushInhibitor.ts index 85d6de8..8a31abf 100644 --- a/src/lib/extensions/BushInhibitor.ts +++ b/src/lib/extensions/BushInhibitor.ts @@ -1,6 +1,15 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { Inhibitor } from 'discord-akairo'; import { BushClient } from './BushClient'; +import { BushCommand } from './BushCommand'; +import { BushSlashMessage } from './BushInteractionMessage'; +import { BushMessage } from './BushMessage'; export class BushInhibitor extends Inhibitor { public declare client: BushClient; + + public exec(message: BushMessage, command: BushCommand): any; + public exec(message: BushMessage | BushSlashMessage, command: BushCommand): any { + super.exec(message, command); + } } diff --git a/src/lib/extensions/BushInteractionMessage.ts b/src/lib/extensions/BushInteractionMessage.ts index ade11ea..62d2519 100644 --- a/src/lib/extensions/BushInteractionMessage.ts +++ b/src/lib/extensions/BushInteractionMessage.ts @@ -1,15 +1,16 @@ import { AkairoMessage } from 'discord-akairo'; import { CommandInteraction } from 'discord.js'; import { BushClient } from './BushClient'; +import { BushCommandUtil } from './BushCommandUtil'; export class BushSlashMessage extends AkairoMessage { + public declare client: BushClient; + public declare util: BushCommandUtil; public constructor( client: BushClient, interaction: CommandInteraction, { slash, replied }: { slash?: boolean; replied?: boolean } ) { super(client, interaction, { slash, replied }); - this.client = client; - this.interaction = interaction; } } diff --git a/src/lib/extensions/BushTaskHandler.ts b/src/lib/extensions/BushTaskHandler.ts index 923e42b..588988d 100644 --- a/src/lib/extensions/BushTaskHandler.ts +++ b/src/lib/extensions/BushTaskHandler.ts @@ -6,7 +6,6 @@ export type BushTaskHandlerOptions = AkairoHandlerOptions; export class BushTaskHandler extends TaskHandler { public constructor(client: BushClient, options: BushTaskHandlerOptions) { super(client, options); - this.client; } declare client: BushClient; } diff --git a/src/lib/models/Ban.ts b/src/lib/models/Ban.ts index 8ba55ec..f4463b8 100644 --- a/src/lib/models/Ban.ts +++ b/src/lib/models/Ban.ts @@ -1,7 +1,6 @@ import { Snowflake } from 'discord.js'; import { DataTypes, Sequelize } from 'sequelize'; import { v4 as uuidv4 } from 'uuid'; -import * as Models from './'; import { BaseModel } from './BaseModel'; export interface BanModel { @@ -64,7 +63,7 @@ export class Ban extends BaseModel<BanModel, BanModelCreationAttributes> impleme type: DataTypes.STRING, allowNull: false, references: { - model: Models.Guild, + model: 'Guilds', key: 'id' } }, @@ -78,11 +77,11 @@ export class Ban extends BaseModel<BanModel, BanModelCreationAttributes> impleme }, modlog: { type: DataTypes.STRING, - allowNull: false - // references: { - // model: Models.Modlog, - // key: 'id' - // } + allowNull: false, + references: { + model: 'ModLogs', + key: 'id' + } } }, { sequelize: sequelize } diff --git a/src/lib/models/Global.ts b/src/lib/models/Global.ts index abe0ab3..842f14b 100644 --- a/src/lib/models/Global.ts +++ b/src/lib/models/Global.ts @@ -80,7 +80,7 @@ export class Global extends BaseModel<GlobalModel, GlobalModelCreationAttributes allowNull: true } }, - { sequelize } + { sequelize: sequelize } ); } } diff --git a/src/lib/models/Guild.ts b/src/lib/models/Guild.ts index c4ae53e..480cc60 100644 --- a/src/lib/models/Guild.ts +++ b/src/lib/models/Guild.ts @@ -8,16 +8,24 @@ export interface GuildModel { prefix: string; autoPublishChannels: string[]; blacklistedChannels: Snowflake[]; + welcomeChannel: Snowflake; + muteRole: Snowflake; } -export type GuildModelCreationAttributes = Optional<GuildModel, 'prefix' | 'autoPublishChannels' | 'blacklistedChannels'>; +export type GuildModelCreationAttributes = Optional< + GuildModel, + 'prefix' | 'autoPublishChannels' | 'blacklistedChannels' | 'welcomeChannel' | 'muteRole' +>; export class Guild extends BaseModel<GuildModel, GuildModelCreationAttributes> implements GuildModel { id: string; prefix: string; autoPublishChannels: string[]; blacklistedChannels: Snowflake[]; - static initModel(seqeulize: Sequelize, client: BushClient): void { + welcomeChannel: Snowflake; + muteRole: Snowflake; + + static initModel(sequelize: Sequelize, client: BushClient): void { Guild.init( { id: { @@ -48,9 +56,17 @@ export class Guild extends BaseModel<GuildModel, GuildModelCreationAttributes> i return this.setDataValue('blacklistedChannels', JSON.stringify(val) as unknown as Snowflake[]); }, allowNull: true + }, + welcomeChannel: { + type: DataTypes.STRING, + allowNull: true + }, + muteRole: { + type: DataTypes.STRING, + allowNull: true } }, - { sequelize: seqeulize } + { sequelize: sequelize } ); } } diff --git a/src/lib/models/Level.ts b/src/lib/models/Level.ts index 426ec1a..e1f30f4 100644 --- a/src/lib/models/Level.ts +++ b/src/lib/models/Level.ts @@ -46,7 +46,6 @@ export class Level extends BaseModel<LevelModel, LevelModelCreationAttributes> { break; } else { i++; - continue; } } return lvl - 1; // I have to do this don't question it ok diff --git a/src/lib/models/Modlog.ts b/src/lib/models/Modlog.ts index 15c5030..94c464d 100644 --- a/src/lib/models/Modlog.ts +++ b/src/lib/models/Modlog.ts @@ -2,18 +2,20 @@ import { DataTypes, Sequelize } from 'sequelize'; import { v4 as uuidv4 } from 'uuid'; import { BaseModel } from './BaseModel'; -export enum ModlogType { +export enum ModLogType { BAN = 'BAN', - TEMPBAN = 'TEMPBAN', + TEMP_BAN = 'TEMP_BAN', KICK = 'KICK', MUTE = 'MUTE', - TEMPMUTE = 'TEMPMUTE', - WARN = 'WARN' + TEMP_MUTE = 'TEMP_MUTE', + WARN = 'WARN', + PUNISHMENT_ROLE = 'PUNISHMENT_ROLE', + TEMP_PUNISHMENT_ROLE = 'TEMP_PUNISHMENT_ROLE' } -export interface ModlogModel { +export interface ModLogModel { id: string; - type: ModlogType; + type: ModLogType; user: string; moderator: string; reason: string; @@ -21,9 +23,9 @@ export interface ModlogModel { guild: string; } -export interface ModlogModelCreationAttributes { +export interface ModLogModelCreationAttributes { id?: string; - type: ModlogType; + type: ModLogType; user: string; moderator: string; reason?: string; @@ -31,9 +33,9 @@ export interface ModlogModelCreationAttributes { guild: string; } -export class Modlog extends BaseModel<ModlogModel, ModlogModelCreationAttributes> implements ModlogModel { +export class ModLog extends BaseModel<ModLogModel, ModLogModelCreationAttributes> implements ModLogModel { id: string; - type: ModlogType; + type: ModLogType; user: string; moderator: string; guild: string; @@ -41,7 +43,7 @@ export class Modlog extends BaseModel<ModlogModel, ModlogModelCreationAttributes duration: number | null; static initModel(sequelize: Sequelize): void { - Modlog.init( + ModLog.init( { id: { type: DataTypes.STRING, @@ -50,7 +52,7 @@ export class Modlog extends BaseModel<ModlogModel, ModlogModelCreationAttributes defaultValue: uuidv4 }, type: { - type: new DataTypes.ENUM('BAN', 'TEMPBAN', 'MUTE', 'TEMPMUTE', 'KICK', 'WARN'), + type: DataTypes.STRING, //# This is not an enum because of a sequelize issue: https://github.com/sequelize/sequelize/issues/2554 allowNull: false }, user: { @@ -70,14 +72,14 @@ export class Modlog extends BaseModel<ModlogModel, ModlogModelCreationAttributes allowNull: true }, guild: { - type: DataTypes.STRING - // references: { - // model: Models.Guild, - // key: 'id' - // } + type: DataTypes.STRING, + references: { + model: 'Guilds', + key: 'id' + } } }, - { sequelize } + { sequelize: sequelize } ); } } diff --git a/src/lib/models/Mute.ts b/src/lib/models/Mute.ts new file mode 100644 index 0000000..273d5b1 --- /dev/null +++ b/src/lib/models/Mute.ts @@ -0,0 +1,90 @@ +import { Snowflake } from 'discord.js'; +import { DataTypes, Sequelize } from 'sequelize'; +import { v4 as uuidv4 } from 'uuid'; +import { BaseModel } from './BaseModel'; + +export interface MuteModel { + id: string; + user: string; + guild: string; + reason: string; + expires: Date; + modlog: string; +} +export interface MuteModelCreationAttributes { + id?: string; + user: string; + guild: string; + reason?: string; + expires?: Date; + modlog: string; +} + +export class Mute extends BaseModel<MuteModel, MuteModelCreationAttributes> implements MuteModel { + /** + * The ID of this mute (no real use just for a primary key) + */ + id: string; + /** + * The user who is muted + */ + user: Snowflake; + /** + * The guild they are muted in + */ + guild: Snowflake; + /** + * The reason they are muted (optional) + */ + reason: string | null; + /** + * The date at which this Mute expires and should be unmuted (optional) + */ + expires: Date | null; + /** + * The ref to the modlog entry + */ + modlog: string; + + static initModel(sequelize: Sequelize): void { + Mute.init( + { + id: { + type: DataTypes.STRING, + primaryKey: true, + allowNull: false, + defaultValue: uuidv4 + }, + user: { + type: DataTypes.STRING, + allowNull: false + }, + guild: { + type: DataTypes.STRING, + allowNull: false, + references: { + model: 'Guilds', + key: 'id' + } + }, + expires: { + type: DataTypes.DATE, + allowNull: true + }, + reason: { + type: DataTypes.STRING, + allowNull: true + }, + modlog: { + type: DataTypes.STRING, + allowNull: false, + references: { + model: 'ModLogs', + key: 'id' + } + } + }, + { sequelize: sequelize } + ); + } +} diff --git a/src/lib/models/PunishmentRole.ts b/src/lib/models/PunishmentRole.ts new file mode 100644 index 0000000..3326dca --- /dev/null +++ b/src/lib/models/PunishmentRole.ts @@ -0,0 +1,93 @@ +import { Snowflake } from 'discord.js'; +import { DataTypes, Sequelize } from 'sequelize'; +import { v4 as uuidv4 } from 'uuid'; +import { BaseModel } from './BaseModel'; + +export interface PunishmentRoleModel { + id: string; + user: string; + guild: string; + reason: string; + expires: Date; + modlog: string; +} +export interface PunishmentRoleModelCreationAttributes { + id?: string; + user: string; + guild: string; + reason?: string; + expires?: Date; + modlog: string; +} + +export class PunishmentRole + extends BaseModel<PunishmentRoleModel, PunishmentRoleModelCreationAttributes> + implements PunishmentRoleModel +{ + /** + * The ID of this punishment role (no real use just for a primary key) + */ + id: string; + /** + * The user who received a role + */ + user: Snowflake; + /** + * The guild they received a role in + */ + guild: Snowflake; + /** + * The reason they received a role (optional) + */ + reason: string | null; + /** + * The date at which this role expires and should be removed (optional) + */ + expires: Date | null; + /** + * The ref to the modlog entry + */ + modlog: string; + + static initModel(sequelize: Sequelize): void { + PunishmentRole.init( + { + id: { + type: DataTypes.STRING, + primaryKey: true, + allowNull: false, + defaultValue: uuidv4 + }, + user: { + type: DataTypes.STRING, + allowNull: false + }, + guild: { + type: DataTypes.STRING, + allowNull: false, + references: { + model: 'Guilds', + key: 'id' + } + }, + expires: { + type: DataTypes.DATE, + allowNull: true + }, + reason: { + type: DataTypes.STRING, + allowNull: true + }, + modlog: { + type: DataTypes.STRING, + allowNull: false, + references: { + model: 'ModLogs', + key: 'id' + } + } + }, + { sequelize: sequelize } + ); + } +} diff --git a/src/lib/models/index.ts b/src/lib/models/index.ts index e38ad69..794c335 100644 --- a/src/lib/models/index.ts +++ b/src/lib/models/index.ts @@ -3,5 +3,7 @@ export * from './BaseModel'; export * from './Global'; export * from './Guild'; export * from './Level'; -export * from './Modlog'; +export * from './ModLog'; +export * from './Mute'; +export * from './PunishmentRole'; export * from './StickyRole'; diff --git a/src/lib/utils/BushCache.ts b/src/lib/utils/BushCache.ts index 947b15d..ffef470 100644 --- a/src/lib/utils/BushCache.ts +++ b/src/lib/utils/BushCache.ts @@ -1,9 +1,13 @@ import { Snowflake } from 'discord.js'; -export class BushCache { +class GlobalCache { public static superUsers = new Array<Snowflake>(); public static disabledCommands = new Array<string>(); public static blacklistedChannels = new Array<Snowflake>(); public static blacklistedGuilds = new Array<Snowflake>(); public static blacklistedUsers = new Array<Snowflake>(); } + +export class BushCache { + public static global = GlobalCache; +} diff --git a/src/lib/utils/BushLogger.ts b/src/lib/utils/BushLogger.ts index 2225bde..6adacfd 100644 --- a/src/lib/utils/BushLogger.ts +++ b/src/lib/utils/BushLogger.ts @@ -105,7 +105,7 @@ export class BushLogger { .setDescription(`**[${header}]** ${this.parseFormatting(this.stripColor(newContent), '', true)}`) .setColor(this.client.util.colors.gray) .setTimestamp(); - this.channelLog({ embeds: [embed] }); + await this.channelLog({ embeds: [embed] }); } /** @@ -125,7 +125,7 @@ export class BushLogger { .setDescription(`**[${header}]** ${this.parseFormatting(this.stripColor(newContent), '', true)}`) .setColor(this.client.util.colors.info) .setTimestamp(); - this.channelLog({ embeds: [embed] }); + await this.channelLog({ embeds: [embed] }); } /** @@ -146,7 +146,7 @@ export class BushLogger { .setDescription(`**[${header}]** ${this.parseFormatting(this.stripColor(newContent), '', true)}`) .setColor(this.client.util.colors.warn) .setTimestamp(); - this.channelLog({ embeds: [embed] }); + await this.channelLog({ embeds: [embed] }); } /** @@ -166,7 +166,7 @@ export class BushLogger { .setDescription(`**[${header}]** ${this.parseFormatting(this.stripColor(newContent), '', true)}`) .setColor(this.client.util.colors.error) .setTimestamp(); - this.channelError({ embeds: [embed] }); + await this.channelError({ embeds: [embed] }); } /** diff --git a/src/lib/utils/CanvasProgressBar.ts b/src/lib/utils/CanvasProgressBar.ts index aa8630a..d870bf9 100644 --- a/src/lib/utils/CanvasProgressBar.ts +++ b/src/lib/utils/CanvasProgressBar.ts @@ -1,10 +1,10 @@ // I just copy pasted this code from stackoverflow don't yell at me if there is issues for it export class CanvasProgressBar { - private x: number; - private y: number; - private w: number; - private h: number; - private color: string; + private readonly x: number; + private readonly y: number; + private readonly w: number; + private readonly h: number; + private readonly color: string; private percentage: number; private p: number; private ctx: CanvasRenderingContext2D; diff --git a/src/listeners/client/ready.ts b/src/listeners/client/ready.ts index a87d216..1a51527 100644 --- a/src/listeners/client/ready.ts +++ b/src/listeners/client/ready.ts @@ -10,15 +10,15 @@ export default class ReadyListener extends BushListener { } public async exec(): Promise<void> { - //@ts-expect-error: ik its private, this is the only time I need to access it outside of its class - const timeStamp = chalk.bgGreen(this.client.logger.getTimeStamp()), - tag = chalk.magenta(this.client.user.tag), - guildCount = chalk.magenta(this.client.guilds.cache.size.toLocaleString()), - userCount = chalk.magenta(this.client.users.cache.size.toLocaleString()); + const tag = `<<${this.client.user.tag}>>`, + guildCount = `<<${this.client.guilds.cache.size.toLocaleString()}>>`, + userCount = `<<${this.client.users.cache.size.toLocaleString()}>>`; - console.log(`${timeStamp} Logged in to ${tag} serving ${guildCount} guilds and ${userCount} users.`); + this.client.logger.success('Ready', `Logged in to ${tag} serving ${guildCount} guilds and ${userCount} users.`); console.log( - chalk.blue(`----------------------------------------------------------------------${this.client.config.dev ? '---' : ''}`) + chalk.blue( + `------------------------------------------------------------------------------${this.client.config.dev ? '---' : ''}` + ) ); this.client.user.setPresence({ diff --git a/src/listeners/commands/commandBlocked.ts b/src/listeners/commands/commandBlocked.ts index febfc93..816005d 100644 --- a/src/listeners/commands/commandBlocked.ts +++ b/src/listeners/commands/commandBlocked.ts @@ -14,7 +14,7 @@ export default class CommandBlockedListener extends BushListener { this.client.console.info( 'CommandBlocked', `<<${message.author.tag}>> tried to run <<${message.util.parsed.command}>> but was blocked because <<${reason}>>.`, - false + true ); const reasons = this.client.consts.BlockedReasons; diff --git a/src/listeners/commands/commandError.ts b/src/listeners/commands/commandError.ts index 7f765ae..85ff11f 100644 --- a/src/listeners/commands/commandError.ts +++ b/src/listeners/commands/commandError.ts @@ -26,13 +26,13 @@ export default class CommandErrorListener extends BushListener { .setColor(this.client.util.colors.error) .setTimestamp(); + await this.client.logger.channelError({ embeds: [errorEmbed] }); if (message) { if (!this.client.config.owners.includes(message.author.id)) { const errorUserEmbed: MessageEmbed = new MessageEmbed() - .setTitle('An error occurred') + .setTitle('A Command Error Occurred') .setColor(this.client.util.colors.error) .setTimestamp(); - await this.client.logger.channelError({ embeds: [errorEmbed] }); if (!command) errorUserEmbed.setDescription(`Oh no! An error occurred. Please give the developers code \`${errorNo}\`.`); else @@ -45,7 +45,7 @@ export default class CommandErrorListener extends BushListener { }); } else { const errorDevEmbed = new MessageEmbed() - .setTitle('An error occurred') + .setTitle('A Command Error Occurred') .setColor(this.client.util.colors.error) .setTimestamp() .setDescription(await this.client.util.codeblock(`${error?.stack}`, 2048, 'js')); diff --git a/src/listeners/commands/commandMissingPermissions.ts b/src/listeners/commands/commandMissingPermissions.ts new file mode 100644 index 0000000..d695d25 --- /dev/null +++ b/src/listeners/commands/commandMissingPermissions.ts @@ -0,0 +1,55 @@ +import { BushCommand } from '../../lib/extensions/BushCommand'; +import { BushListener } from '../../lib/extensions/BushListener'; +import { BushMessage } from '../../lib/extensions/BushMessage'; + +export default class CommandMissingPermissionsListener extends BushListener { + public constructor() { + super('commandMissingPermissions', { + emitter: 'commandHandler', + event: 'missingPermissions', + category: 'commands' + }); + } + + public async exec( + message: BushMessage, + command: BushCommand | null | undefined, + type: 'client' | 'user', + missing: Array<string> + ): Promise<void> { + const niceMissing = []; + missing.forEach((missing) => { + if (this.client.consts.mappings.permissions[missing]) { + niceMissing.push(this.client.consts.mappings.permissions[missing].name); + } else { + niceMissing.push(missing); + } + }); + + const discordFormat = this.client.util.oxford(this.client.util.surroundArray(niceMissing, '`'), 'and', ''); + const consoleFormat = this.client.util.oxford(this.client.util.surroundArray(niceMissing, '<<', '>>'), 'and', ''); + this.client.console.info( + 'CommandMissingPermissions', + `<<${message.author.tag}>> tried to run <<${ + command?.id + }>> but could not because <<${type}>> is missing the ${consoleFormat} permissions${missing.length ? 's' : ''}.` + ); + if (type == 'client') { + await message.util + .reply( + `${this.client.util.emojis.error} I am missing the ${discordFormat} permission${ + missing.length ? 's' : '' + } required for the \`${command?.id}\` command.` + ) + .catch(() => {}); + } else if (type == 'user') { + await message.util + .reply( + `${this.client.util.emojis.error} You are missing the ${discordFormat} permission${ + missing.length ? 's' : '' + } required for the \`${command?.id}\` command.` + ) + .catch(() => {}); + } + } +} diff --git a/src/listeners/commands/commandStarted.ts b/src/listeners/commands/commandStarted.ts index 8c860f8..9266fc7 100644 --- a/src/listeners/commands/commandStarted.ts +++ b/src/listeners/commands/commandStarted.ts @@ -15,7 +15,7 @@ export default class CommandStartedListener extends BushListener { `The <<${command.id}>> command was used by <<${message.author.tag}>> in ${ message.channel.type === 'dm' ? `their <<DMs>>` : `<<#${message.channel.name}>> in <<${message.guild?.name}>>` }.`, - false // I don't want to spam the log channel when people use commands + true //// I don't want to spam the log channel when people use commands ); } } diff --git a/src/listeners/commands/slashBlocked.ts b/src/listeners/commands/slashBlocked.ts index e87ba70..e64253a 100644 --- a/src/listeners/commands/slashBlocked.ts +++ b/src/listeners/commands/slashBlocked.ts @@ -14,7 +14,7 @@ export default class SlashBlockedListener extends BushListener { this.client.console.info( 'SlashBlocked', `<<${message.author.tag}>> tried to run <<${message.util.parsed.command}>> but was blocked because <<${reason}>>.`, - false + true ); const reasons = this.client.consts.BlockedReasons; diff --git a/src/listeners/commands/slashCommandError.ts b/src/listeners/commands/slashCommandError.ts index b9123e8..1a5f293 100644 --- a/src/listeners/commands/slashCommandError.ts +++ b/src/listeners/commands/slashCommandError.ts @@ -18,20 +18,21 @@ export default class SlashCommandErrorListener extends BushListener { .setDescription( stripIndents`**User:** ${message.author} (${message.author.tag}) **Slash Command:** ${command} - **Channel:** ${message.channel} (${message.channel.id}) - **Message:** [link](https://discord.com/${message.guild.id}/${message.guild.id}/${message.id})` + **Channel:** ${message.channel || message.interaction.user?.tag} ${message.channel ? `(${message.channel?.id})` : ''} + **Message:** [link](https://discord.com/${message.guild?.id}/${message.channel?.id}/${message.id})` ) .addField('Error', await this.client.util.codeblock(`${error?.stack}`, 1024, 'js')) .setColor(this.client.util.colors.error) .setTimestamp(); + await this.client.logger.channelError({ embeds: [errorEmbed] }); if (message) { + const channel = message.channel?.name || message.interaction.user.tag; if (!this.client.config.owners.includes(message.author.id)) { const errorUserEmbed: MessageEmbed = new MessageEmbed() - .setTitle('An error occurred') + .setTitle('A Slash Command Error Occurred') .setColor(this.client.util.colors.error) .setTimestamp(); - await this.client.logger.channelError({ embeds: [errorEmbed] }); if (!command) errorUserEmbed.setDescription(`Oh no! An error occurred. Please give the developers code \`${errorNo}\`.`); else @@ -39,22 +40,20 @@ export default class SlashCommandErrorListener extends BushListener { `Oh no! While running the command \`${command.id}\`, an error occurred. Please give the developers code \`${errorNo}\`.` ); await message.util.send({ embeds: [errorUserEmbed] }).catch((e) => { - const channel = message.channel.type === 'dm' ? message.channel.recipient.tag : message.channel.name; this.client.console.warn('SlashError', `Failed to send user error embed in <<${channel}>>:\n` + e?.stack); }); } else { const errorDevEmbed = new MessageEmbed() - .setTitle('An error occurred') + .setTitle('A Slash Command Error Occurred') .setColor(this.client.util.colors.error) .setTimestamp() .setDescription(await this.client.util.codeblock(`${error?.stack}`, 2048, 'js')); await message.util.send({ embeds: [errorDevEmbed] }).catch((e) => { - const channel = message.channel.type === 'dm' ? message.channel.recipient.tag : message.channel.name; this.client.console.warn('SlashError', `Failed to send owner error stack in <<${channel}>>.` + e?.stack); }); } } - const channel = message.channel.type === 'dm' ? message.channel.recipient.tag : message.channel.name; + const channel = message.channel?.name || message.interaction.user.tag; this.client.console.error( 'SlashError', `an error occurred with the <<${command}>> command in <<${channel}>> triggered by <<${message?.author?.tag}>>:\n` + diff --git a/src/listeners/commands/slashMissingPermissions.ts b/src/listeners/commands/slashMissingPermissions.ts new file mode 100644 index 0000000..1f3599f --- /dev/null +++ b/src/listeners/commands/slashMissingPermissions.ts @@ -0,0 +1,58 @@ +import { Command } from 'discord-akairo'; +import { CommandInteraction } from 'discord.js'; +import { BushListener } from '../../lib/extensions/BushListener'; + +export default class SlashMissingPermissionsListener extends BushListener { + public constructor() { + super('slashMissingPermissions', { + emitter: 'commandHandler', + event: 'slashMissingPermissions', + category: 'slashCommands' + }); + } + + public async exec( + interaction: CommandInteraction, + command: Command, + type: 'user' | 'client', + missing?: string[] + ): Promise<void> { + const niceMissing = []; + missing.forEach((missing) => { + if (this.client.consts.mappings.permissions[missing]) { + niceMissing.push(this.client.consts.mappings.permissions[missing].name); + } else { + niceMissing.push(missing); + } + }); + + const discordFormat = this.client.util.oxford(this.client.util.surroundArray(niceMissing, '`'), 'and', ''); + const consoleFormat = this.client.util.oxford(this.client.util.surroundArray(niceMissing, '<<', '>>'), 'and', ''); + this.client.console.info( + 'CommandMissingPermissions', + `<<${interaction.user.tag}>> tried to run <<${ + command?.id + }>> but could not because <<${type}>> is missing the ${consoleFormat} permissions${missing.length ? 's' : ''}.`, + true + ); + if (type == 'client') { + await this.client.util + .slashRespond( + interaction, + `${this.client.util.emojis.error} I am missing the ${discordFormat} permission${ + missing.length ? 's' : '' + } required for the \`${command?.id}\` command.` + ) + .catch(() => {}); + } else if (type == 'user') { + await this.client.util + .slashRespond( + interaction, + `${this.client.util.emojis.error} You are missing the ${discordFormat} permission${ + missing.length ? 's' : '' + } required for the \`${command?.id}\` command.` + ) + .catch(() => {}); + } + } +} diff --git a/src/listeners/commands/slashStarted.ts b/src/listeners/commands/slashStarted.ts index 6a45546..c6b966a 100644 --- a/src/listeners/commands/slashStarted.ts +++ b/src/listeners/commands/slashStarted.ts @@ -1,5 +1,5 @@ -import { Message } from 'discord.js'; import { BushCommand } from '../../lib/extensions/BushCommand'; +import { BushSlashMessage } from '../../lib/extensions/BushInteractionMessage'; import { BushListener } from '../../lib/extensions/BushListener'; export default class SlashStartedListener extends BushListener { @@ -9,13 +9,13 @@ export default class SlashStartedListener extends BushListener { event: 'slashStarted' }); } - exec(message: Message, command: BushCommand): void { + exec(message: BushSlashMessage, command: BushCommand): void { this.client.logger.info( 'SlashCommand', `The <<${command.id}>> command was used by <<${message.author.tag}>> in ${ - message.channel.type === 'dm' ? `their <<DMs>>` : `<<#${message.channel.name}>> in <<${message.guild?.name}>>` + !message.channel ? `their <<DMs>>` : `<<#${message.channel.name}>> in <<${message.guild?.name}>>` }.`, - false // I don't want to spam the log channel when people use commands + true //// I don't want to spam the log channel when people use commands ); } } diff --git a/src/listeners/message/level.ts b/src/listeners/message/level.ts index 74c4db8..9f413e5 100644 --- a/src/listeners/message/level.ts +++ b/src/listeners/message/level.ts @@ -32,8 +32,7 @@ export default class LevelListener extends BushListener { this.client.logger.error('LevelMessageListener', e); return false; }); - if (success) - await this.client.logger.verbose(`LevelMessageListener`, `Gave <<${xpToGive}>> XP to <<${message.author.tag}>>.`); + if (success) this.client.logger.verbose(`LevelMessageListener`, `Gave <<${xpToGive}>> XP to <<${message.author.tag}>>.`); this.levelCooldowns.add(message.author.id); setTimeout(() => this.levelCooldowns.delete(message.author.id), 60_000); } diff --git a/src/listeners/other/consoleListener.ts b/src/listeners/other/consoleListener.ts index 50c0cf3..d1e318f 100644 --- a/src/listeners/other/consoleListener.ts +++ b/src/listeners/other/consoleListener.ts @@ -30,21 +30,6 @@ export default class ConsoleListener extends BushListener { } catch (e) { console.error(e); } - } /* else if (line.startsWith('reload')) { - exec('npx tsc', (error) => { - if (error) { - return this.client.console.error('Reload', `Error recompiling, \`${error.message}\``); - } - try { - this.client.commandHandler.reloadAll(); - this.client.listenerHandler.reloadAll(); - } catch (e) { - return this.client.console.error('Reload', e); - } - this.client.console.success('Reload', 'Reloaded successfully.'); - }); - } else if (line.startsWith('stop') || line.startsWith('exit')) { - process.exit(); - } */ + } } } diff --git a/src/listeners/other/promiseRejection.ts b/src/listeners/other/promiseRejection.ts index 2d7e316..143659a 100644 --- a/src/listeners/other/promiseRejection.ts +++ b/src/listeners/other/promiseRejection.ts @@ -10,7 +10,7 @@ export default class PromiseRejectionListener extends BushListener { public async exec(error: Error): Promise<void> { this.client.console.error('PromiseRejection', 'An unhanded promise rejection occurred:\n' + error.stack, false); - await this.client.console.channelError({ + this.client.console.channelError({ embeds: [ { title: 'Unhandled promise rejection', diff --git a/src/tasks/removePunishmentRole.ts b/src/tasks/removePunishmentRole.ts new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/tasks/removePunishmentRole.ts diff --git a/src/tasks/unmute.ts b/src/tasks/unmute.ts index e69de29..33cbd92 100644 --- a/src/tasks/unmute.ts +++ b/src/tasks/unmute.ts @@ -0,0 +1,45 @@ +import { DiscordAPIError } from 'discord.js'; +import { Op } from 'sequelize'; +import { BushTask } from '../lib/extensions/BushTask'; +import { Guild } from '../lib/models'; +import { Mute } from '../lib/models/Mute'; + +export default class UnmuteTask extends BushTask { + constructor() { + super('unmute', { + delay: 30_000, // 1/2 min + runOnStart: true + }); + } + async exec(): Promise<void> { + const rows = await Mute.findAll({ + where: { + [Op.and]: [ + { + expires: { + [Op.lt]: new Date() // Find all rows with an expiry date before now + } + } + ] + } + }); + this.client.logger.verbose(`UnmuteTask`, `Queried mutes, found <<${rows.length}>> expired mutes.`); + for (const row of rows) { + const guild = this.client.guilds.cache.get(row.guild); + const muteRole = (await Guild.findByPk(row.guild)).muteRole; + if (!guild) { + await row.destroy(); + continue; + } + try { + await (await guild.members.fetch(row.user)).roles.remove(muteRole); + } catch (e) { + if (e instanceof DiscordAPIError) { + // ignore + } else throw e; + } + await row.destroy(); + this.client.logger.verbose(`UnmuteTask`, `Unmuted ${row.user}`); + } + } +} diff --git a/src/tasks/updateCache.ts b/src/tasks/updateCache.ts index 6c1f098..3f213f2 100644 --- a/src/tasks/updateCache.ts +++ b/src/tasks/updateCache.ts @@ -1,15 +1,26 @@ +import { BushClient } from '../lib/extensions/BushClient'; import { BushTask } from '../lib/extensions/BushTask'; import { Global } from '../lib/models'; +import * as config from './../config/options'; export default class UpdateCacheTask extends BushTask { constructor() { super('updateCache', { delay: 300_000, // 5 minutes - runOnStart: true + runOnStart: false // done in preinit task }); } async exec(): Promise<void> { - const environment = this.client.config.dev ? 'development' : 'production'; + await this.updateCache(this.client); + await this.client.logger.verbose(`UpdateCache`, `Updated cache.`); + } + + async init(client: BushClient): Promise<void> { + await this.updateCache(client); + } + + async updateCache(client: BushClient): Promise<void> { + const environment = config.dev ? 'development' : 'production'; const row = (await Global.findByPk(environment)) || (await Global.create({ @@ -22,8 +33,7 @@ export default class UpdateCacheTask extends BushTask { })); for (const option in row) { - if (this.client.cache[option]) this.client.cache[option] = row[option]; + if (client.cache[option]) this.client.cache[option] = row[option]; } - this.client.logger.verbose(`UpdateCache`, `Updated cache.`); } } |