diff options
Diffstat (limited to 'src')
25 files changed, 570 insertions, 208 deletions
diff --git a/src/commands/config/prefix.ts b/src/commands/config/prefix.ts index 9ddf81b..3bb717b 100644 --- a/src/commands/config/prefix.ts +++ b/src/commands/config/prefix.ts @@ -39,6 +39,7 @@ export default class PrefixCommand extends BushCommand { id: guild.id }); } + // this.client.console.debug(row); if (prefix) { row.prefix = prefix; await row.save(); diff --git a/src/commands/dev/setLevel.ts b/src/commands/dev/setLevel.ts index 5a702e9..58c01dd 100644 --- a/src/commands/dev/setLevel.ts +++ b/src/commands/dev/setLevel.ts @@ -21,15 +21,15 @@ export default class SetLevelCommand extends BushCommand { type: 'user', prompt: { start: 'What user would you like to change the level of?', - retry: 'Invalid user. What user would you like to change the level of?' + retry: '{error} Choose a valid user to change the level of.' } }, { id: 'level', type: 'number', prompt: { - start: 'What level would you like to set?', - retry: 'Invalid user. What level would you like to set?' + start: 'What level would you like to set the user to?', + retry: '{error} Choose a valid level to set the user to.' } } ], diff --git a/src/commands/info/help.ts b/src/commands/info/help.ts index 9caae24..2ffac1b 100644 --- a/src/commands/info/help.ts +++ b/src/commands/info/help.ts @@ -1,4 +1,4 @@ -import { MessageEmbed } from 'discord.js'; +import { MessageActionRow, MessageButton, MessageEmbed } from 'discord.js'; import { BushCommand } from '../../lib/extensions/BushCommand'; import { BushInteractionMessage } from '../../lib/extensions/BushInteractionMessage'; import { BushMessage } from '../../lib/extensions/BushMessage'; @@ -9,22 +9,33 @@ export default class HelpCommand extends BushCommand { aliases: ['help'], category: 'info', description: { - content: 'Shows the commands of the bot', - usage: 'help', - examples: ['help'] + content: 'Displays a list of commands, or detailed information for a specific command.', + usage: 'help [command]', + examples: ['help prefix'] }, clientPermissions: ['EMBED_LINKS'], args: [ { id: 'command', - type: 'commandAlias' + type: 'commandAlias', + match: 'content', + prompt: { + start: 'What command do you need help with?', + retry: '{error} Choose a valid command to find help for.', + optional: true + } + }, + { + id: 'showHidden', + match: 'flag', + flag: '--hidden' } ], slashOptions: [ { type: 'STRING', name: 'command', - description: 'The command to get help for', + description: `The command you would like to find information about.`, required: false } ], @@ -32,46 +43,73 @@ export default class HelpCommand extends BushCommand { }); } - private generateEmbed(command?: BushCommand, message?: BushInteractionMessage | BushMessage): MessageEmbed { + public async exec( + message: BushMessage | BushInteractionMessage, + args: { command: BushCommand | string; showHidden?: boolean } + ): Promise<unknown> { const prefix = this.client.config.dev ? 'dev ' : message.util.parsed.prefix; + let ButtonRow: MessageActionRow; + if (!this.client.config.dev) { + ButtonRow = new MessageActionRow().addComponents( + new MessageButton({ + style: 'LINK', + label: 'Invite Me', + url: `https://discord.com/api/oauth2/authorize?client_id=${this.client.user.id}&permissions=2147483647&scope=bot%20applications.commands` + }) + ); + } + const isOwner = this.client.isOwner(message.author); + const isSuperUser = this.client.isSuperUser(message.author); + const command = args.command + ? typeof args.command === 'string' + ? this.client.commandHandler.modules.get(args.command) || null + : args.command + : null; + if (!isOwner) args.showHidden = false; if (!command) { - const embed = new MessageEmbed() - .setFooter(`For more information about a command use '${prefix}help <command>'`) - .setTimestamp() - .setColor(this.client.util.colors.default); - for (const category of this.handler.categories.values()) { - embed.addField( - `${category.id.replace(/(\b\w)/gi, (lc): string => lc.toUpperCase())}`, - `${category - .filter((cmd): boolean => cmd.aliases.length > 0) - .map((cmd): string => `\`${cmd.aliases[0]}\``) - .join(' ')}` - ); + const embed = new MessageEmbed().setColor(this.client.util.colors.default).setTimestamp(); + if (message.guild) { + embed.setFooter(`For more information about a command use '${prefix}help <command>'`); + } + for (const [, category] of this.handler.categories) { + const categoryFilter = category.filter((command) => { + if (command.hidden && !args.showHidden) return false; + if (command.channel == 'guild' && !message.guild && !args.showHidden) return false; + if (command.ownerOnly && !isOwner) return false; + if (command.superUserOnly && !isSuperUser) { + return false; + } + if (command.restrictedGuilds?.includes(message.guild.id) == !true && !args.showHidden) return false; + return true; + }); + const categoryNice = category.id + .replace(/(\b\w)/gi, (lc): string => lc.toUpperCase()) + .replace(/'(S)/g, (letter): string => letter.toLowerCase()); + const categoryCommands = categoryFilter + .filter((cmd): boolean => cmd.aliases.length > 0) + .map((cmd): string => `\`${cmd.aliases[0]}\``); + if (categoryCommands.length > 0) { + embed.addField(`${categoryNice}`, `${categoryCommands.join(' ')}`); + } } - return embed; - } else { - const embed = new MessageEmbed() - .setColor([155, 200, 200]) - .setTitle(`\`${command.description.usage ? command.description.usage : ''}\``) - .addField( - 'Description', - `${command.description.content ? command.description.content : ''} ${command.ownerOnly ? '\n__Owner Only__' : ''}` - ); + return await message.util.reply({ embeds: [embed], components: ButtonRow ? [ButtonRow] : undefined }); + } + + const embed = new MessageEmbed() + .setColor(this.client.util.colors.default) + .setTitle(`\`${command.description?.usage ? command.description.usage : 'This command does not have usages.'}\``) + .addField( + 'Description', + `${command.description?.content ? command.description.content : '*This command does not have a description.*'} ${ + command.ownerOnly ? '\n__Dev Only__' : '' + } ${command.superUserOnly ? '\n__Super User Only__' : ''}` + ); - if (command.aliases.length > 1) embed.addField('Aliases', `\`${command.aliases.join('` `')}\``, true); - if (command.description.examples && command.description.examples.length) - embed.addField('Examples', `\`${command.description.examples.join('`\n`')}\``, true); - return embed; + if (command.aliases?.length > 1) embed.addField('Aliases', `\`${command.aliases.join('` `')}\``, true); + if (command.description?.examples && command.description.examples.length) { + embed.addField('Examples', `\`${command.description.examples.join('`\n`')}\``, true); } - } - public async exec( - message: BushMessage | BushInteractionMessage, - { command }: { command: BushCommand | string } - ): Promise<void> { - const parsedCommand = message.util.isSlash - ? (this.handler.findCommand(command as string) as BushCommand) - : (command as BushCommand); - await message.util.send({ embeds: [this.generateEmbed(parsedCommand, message)] }); + return await message.util.reply({ embeds: [embed], components: ButtonRow ? [ButtonRow] : undefined }); } } diff --git a/src/commands/info/ping.ts b/src/commands/info/ping.ts index 5552573..fb93c50 100644 --- a/src/commands/info/ping.ts +++ b/src/commands/info/ping.ts @@ -26,6 +26,7 @@ export default class PingCommand extends BushCommand { .addField('Bot Latency', botLatency, true) .addField('API Latency', apiLatency, true) .setFooter(message.author.username, message.author.displayAvatarURL({ dynamic: true })) + .setColor(this.client.util.colors.default) .setTimestamp(); await sentMessage.edit({ content: null, @@ -44,6 +45,7 @@ export default class PingCommand extends BushCommand { .addField('Bot Latency', botLatency, true) .addField('API Latency', apiLatency, true) .setFooter(message.interaction.user.username, message.interaction.user.displayAvatarURL({ dynamic: true })) + .setColor(this.client.util.colors.default) .setTimestamp(); await message.interaction.editReply({ content: null, diff --git a/src/commands/moderation/role.ts b/src/commands/moderation/role.ts index 1b82245..f0918f0 100644 --- a/src/commands/moderation/role.ts +++ b/src/commands/moderation/role.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-empty-function */ -import { ApplicationCommandOptionType } from 'discord-api-types'; import { GuildMember, Message, Role } from 'discord.js'; import { BushCommand } from '../../lib/extensions/BushCommand'; import AllowedMentions from '../../lib/utils/AllowedMentions'; @@ -56,15 +55,15 @@ export default class RoleCommand extends BushCommand { } } ], - slashCommandOptions: [ + slashOptions: [ { - type: ApplicationCommandOptionType.USER, + type: 'USER', name: 'user', description: 'The user to add/remove the role on', required: true }, { - type: ApplicationCommandOptionType.ROLE, + type: 'ROLE', name: 'role', description: 'The role to add/remove', required: true @@ -75,7 +74,7 @@ export default class RoleCommand extends BushCommand { public async exec(message: Message, { user, role }: { user: GuildMember; role: Role }): Promise<unknown> { if (!message.member.permissions.has('MANAGE_ROLES') && !this.client.ownerID.includes(message.author.id)) { - const mappedRole = this.client.util.moulberryBushRoleMap.find((m) => m.id === role.id); + const mappedRole = this.client.consts.moulberryBushRoleMap.find((m) => m.id === role.id); if (!mappedRole || !this.roleWhitelist[mappedRole.name]) { return message.util.reply({ content: `${this.client.util.emojis.error} <@&${role.id}> is not whitelisted, and you do not have manage roles permission.`, @@ -83,7 +82,7 @@ export default class RoleCommand extends BushCommand { }); } const allowedRoles = this.roleWhitelist[mappedRole.name].map((r) => { - return this.client.util.moulberryBushRoleMap.find((m) => m.name === r).id; + return this.client.consts.moulberryBushRoleMap.find((m) => m.name === r).id; }); if (!message.member.roles.cache.some((role) => allowedRoles.includes(role.id))) { return message.util.reply({ diff --git a/src/lib/extensions/BushClient.ts b/src/lib/extensions/BushClient.ts index 8592281..ad22cfe 100644 --- a/src/lib/extensions/BushClient.ts +++ b/src/lib/extensions/BushClient.ts @@ -9,6 +9,7 @@ import * as config from '../../config/options'; import * as Models from '../models'; import AllowedMentions from '../utils/AllowedMentions'; import { BushCache } from '../utils/BushCache'; +import { BushConstants } from '../utils/BushConstants'; import { BushLogger } from '../utils/BushLogger'; import { BushClientUtil } from './BushClientUtil'; import { BushCommandHandler } from './BushCommandHandler'; @@ -34,6 +35,7 @@ export class BushClient extends AkairoClient { public declare ownerID: Snowflake[]; public db: Sequelize; public logger: BushLogger; + public constants = BushConstants; constructor(config: BotConfig) { super( { @@ -107,7 +109,7 @@ export class BushClient extends AkairoClient { dialect: 'postgres', host: this.config.db.host, port: this.config.db.port, - logging: false + logging: (a, b) => this.logger.debug(a) }); this.logger = new BushLogger(this); } @@ -116,6 +118,10 @@ export class BushClient extends AkairoClient { return this.logger; } + get consts(): typeof BushConstants { + return this.constants; + } + // Initialize everything private async _init(): Promise<void> { this.commandHandler.useListenerHandler(this.listenerHandler); @@ -152,17 +158,20 @@ export class BushClient extends AkairoClient { public async dbPreInit(): Promise<void> { try { await this.db.authenticate(); + Models.Global.initModel(this.db); Models.Guild.initModel(this.db, this); Models.Modlog.initModel(this.db); Models.Ban.initModel(this.db); Models.Level.initModel(this.db); + Models.StickyRole.initModel(this.db); await this.db.sync(); // Sync all tables to fix everything if updated this.console.success('Startup', `Successfully connected to <<database>>.`, false); } catch (error) { - this.console.error('Startup', `Failed to connect to <<database>> with error:\n` + error, false); + this.console.error('Startup', `Failed to connect to <<database>> with error:\n` + error?.stack, false); } } + /** Starts the bot */ public async start(): Promise<void> { try { await this._init(); @@ -173,6 +182,7 @@ export class BushClient extends AkairoClient { } } + /** Logs out, terminates the connection to Discord, and destroys the client. */ public destroy(relogin = false): void | Promise<string> { super.destroy(); if (relogin) { diff --git a/src/lib/extensions/BushClientUtil.ts b/src/lib/extensions/BushClientUtil.ts index 2ecdc42..6687cb0 100644 --- a/src/lib/extensions/BushClientUtil.ts +++ b/src/lib/extensions/BushClientUtil.ts @@ -267,34 +267,6 @@ export class BushClientUtil extends ClientUtil { return apiRes.uuid.replace(/-/g, ''); } - public moulberryBushRoleMap = [ - { name: '*', id: '792453550768390194' }, - { name: 'Admin Perms', id: '746541309853958186' }, - { name: 'Sr. Moderator', id: '782803470205190164' }, - { name: 'Moderator', id: '737308259823910992' }, - { name: 'Helper', id: '737440116230062091' }, - { name: 'Trial Helper', id: '783537091946479636' }, - { name: 'Contributor', id: '694431057532944425' }, - { name: 'Giveaway Donor', id: '784212110263451649' }, - { name: 'Giveaway (200m)', id: '810267756426690601' }, - { name: 'Giveaway (100m)', id: '801444430522613802' }, - { name: 'Giveaway (50m)', id: '787497512981757982' }, - { name: 'Giveaway (25m)', id: '787497515771232267' }, - { name: 'Giveaway (10m)', id: '787497518241153025' }, - { name: 'Giveaway (5m)', id: '787497519768403989' }, - { name: 'Giveaway (1m)', id: '787497521084891166' }, - { name: 'Suggester', id: '811922322767609877' }, - { name: 'Partner', id: '767324547312779274' }, - { name: 'Level Locked', id: '784248899044769792' }, - { name: 'No Files', id: '786421005039173633' }, - { name: 'No Reactions', id: '786421270924361789' }, - { name: 'No Links', id: '786421269356740658' }, - { name: 'No Bots', id: '786804858765312030' }, - { name: 'No VC', id: '788850482554208267' }, - { name: 'No Giveaways', id: '808265422334984203' }, - { name: 'No Support', id: '790247359824396319' } - ]; - /** Paginates an array of embeds using buttons. */ public async buttonPaginate( message: BushMessage, @@ -487,83 +459,26 @@ export class BushClientUtil extends ClientUtil { public getConfigChannel(channel: 'log' | 'error' | 'dm'): Promise<TextChannel> { return this.client.channels.fetch(this.client.config.channels[channel]) as Promise<TextChannel>; } -} - -// 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 percentage: number; - private p: number; - private ctx: CanvasRenderingContext2D; - - constructor( - ctx: CanvasRenderingContext2D, - dimension: { x: number; y: number; width: number; height: number }, - color: string, - percentage: number - ) { - ({ x: this.x, y: this.y, width: this.w, height: this.h } = dimension); - this.color = color; - this.percentage = percentage; - this.p; - this.ctx = ctx; - } - draw(): void { - // ----------------- - this.p = this.percentage * this.w; - if (this.p <= this.h) { - this.ctx.beginPath(); - this.ctx.arc( - this.h / 2 + this.x, - this.h / 2 + this.y, - this.h / 2, - Math.PI - Math.acos((this.h - this.p) / this.h), - Math.PI + Math.acos((this.h - this.p) / this.h) - ); - this.ctx.save(); - this.ctx.scale(-1, 1); - this.ctx.arc( - this.h / 2 - this.p - this.x, - this.h / 2 + this.y, - this.h / 2, - Math.PI - Math.acos((this.h - this.p) / this.h), - Math.PI + Math.acos((this.h - this.p) / this.h) - ); - this.ctx.restore(); - this.ctx.closePath(); - } else { - this.ctx.beginPath(); - this.ctx.arc(this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, Math.PI / 2, (3 / 2) * Math.PI); - this.ctx.lineTo(this.p - this.h + this.x, 0 + this.y); - this.ctx.arc(this.p - this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, (3 / 2) * Math.PI, Math.PI / 2); - this.ctx.lineTo(this.h / 2 + this.x, this.h + this.y); - this.ctx.closePath(); - } - this.ctx.fillStyle = this.color; - this.ctx.fill(); - } - - // showWholeProgressBar(){ - // this.ctx.beginPath(); - // this.ctx.arc(this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, Math.PI / 2, 3 / 2 * Math.PI); - // this.ctx.lineTo(this.w - this.h + this.x, 0 + this.y); - // this.ctx.arc(this.w - this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, 3 / 2 *Math.PI, Math.PI / 2); - // this.ctx.lineTo(this.h / 2 + this.x, this.h + this.y); - // this.ctx.strokeStyle = '#000000'; - // this.ctx.stroke(); - // this.ctx.closePath(); - // } - - get PPercentage(): number { - return this.percentage * 100; - } - - set PPercentage(x: number) { - this.percentage = x / 100; + /** + * Takes an array and combines the elements using the supplied conjunction. + * + * @param {string[]} array The array to combine. + * @param {string} conjunction The conjunction to use. + * @param {string} ifEmpty What to return if the array is empty. + * @returns The combined elements or `ifEmpty` + * + * @example + * const permissions = oxford(['ADMINISTRATOR', 'SEND_MESSAGES', 'MANAGE_MESSAGES'], 'and', 'none'); + * console.log(permissions); // ADMINISTRATOR, SEND_MESSAGES and MANAGE_MESSAGES + */ + public oxford(array: string[], conjunction: string, ifEmpty: string): string { + const l = array.length; + if (!l) return ifEmpty; + if (l < 2) return array[0]; + if (l < 3) return array.join(` ${conjunction} `); + array = array.slice(); + array[l - 1] = `${conjunction} ${array[l - 1]}`; + return array.join(', '); } } diff --git a/src/lib/extensions/BushCommand.ts b/src/lib/extensions/BushCommand.ts index edd3c31..8358c46 100644 --- a/src/lib/extensions/BushCommand.ts +++ b/src/lib/extensions/BushCommand.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Command, CommandOptions } from 'discord-akairo'; -import { APIApplicationCommandOption } from 'discord-api-types'; import { Snowflake } from 'discord.js'; import { BushClient } from './BushClient'; +import { BushCommandHandler } from './BushCommandHandler'; import { BushInteractionMessage } from './BushInteractionMessage'; import { BushMessage } from './BushMessage'; @@ -11,7 +11,6 @@ export interface BushCommandOptions extends CommandOptions { hidden?: boolean; restrictedChannels?: Snowflake[]; restrictedGuilds?: Snowflake[]; - slashCommandOptions?: APIApplicationCommandOption[]; description: { content: string; usage: string; @@ -21,6 +20,7 @@ export interface BushCommandOptions extends CommandOptions { 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[]; @@ -37,9 +37,7 @@ export class BushCommand extends Command { } public exec(message: BushMessage, args: any): any; - // @ts-ignore: They are close enough public exec(message: BushMessage | BushInteractionMessage, args: any): any { - // @ts-ignore: They are close enough super.exec(message, args); } } diff --git a/src/lib/extensions/BushCommandUtil.ts b/src/lib/extensions/BushCommandUtil.ts new file mode 100644 index 0000000..b4084bd --- /dev/null +++ b/src/lib/extensions/BushCommandUtil.ts @@ -0,0 +1,10 @@ +import { CommandUtil, ParsedComponentData } from 'discord-akairo'; +import { BushCommand } from './BushCommand'; + +export interface BushParsedComponentData extends ParsedComponentData { + command?: BushCommand; +} + +export class BushCommandUtil extends CommandUtil { + declare parsed?: BushParsedComponentData; +} diff --git a/src/lib/extensions/BushMessage.ts b/src/lib/extensions/BushMessage.ts index e7146f6..afa6bde 100644 --- a/src/lib/extensions/BushMessage.ts +++ b/src/lib/extensions/BushMessage.ts @@ -1,8 +1,10 @@ import { DMChannel, Message, NewsChannel, TextChannel } from 'discord.js'; import { BushClient } from './BushClient'; +import { BushCommandUtil } from './BushCommandUtil'; export class BushMessage extends Message { declare client: BushClient; + declare util: BushCommandUtil; constructor(client: BushClient, data: unknown, channel: TextChannel | DMChannel | NewsChannel) { super(client, data, channel); this.client = client; diff --git a/src/lib/models/Global.ts b/src/lib/models/Global.ts new file mode 100644 index 0000000..65f51c4 --- /dev/null +++ b/src/lib/models/Global.ts @@ -0,0 +1,50 @@ +import { Snowflake } from 'discord.js'; +import { DataTypes, Optional, Sequelize } from 'sequelize'; +import { BaseModel } from './BaseModel'; + +export interface GlobalModel { + superUsers: Snowflake[]; + disabledCommands: string[]; + blacklistedUsers: Snowflake[]; + blacklistedGuilds: Snowflake[]; + blacklistedChannels: Snowflake[]; +} +export type GlobalModelCreationAttributes = Optional< + GlobalModel, + 'superUsers' | 'disabledCommands' | 'blacklistedUsers' | 'blacklistedGuilds' | 'blacklistedChannels' +>; + +export class Global extends BaseModel<GlobalModel, GlobalModelCreationAttributes> implements GlobalModel { + superUsers: Snowflake[]; + disabledCommands: string[]; + blacklistedUsers: Snowflake[]; + blacklistedGuilds: Snowflake[]; + blacklistedChannels: Snowflake[]; + static initModel(sequelize: Sequelize): void { + Global.init( + { + superUsers: { + type: DataTypes.ARRAY(DataTypes.STRING), + allowNull: true + }, + disabledCommands: { + type: DataTypes.ARRAY(DataTypes.STRING), + allowNull: true + }, + blacklistedUsers: { + type: DataTypes.ARRAY(DataTypes.STRING), + allowNull: true + }, + blacklistedGuilds: { + type: DataTypes.ARRAY(DataTypes.STRING), + allowNull: true + }, + blacklistedChannels: { + type: DataTypes.ARRAY(DataTypes.STRING), + allowNull: true + } + }, + { sequelize } + ); + } +} diff --git a/src/lib/models/Guild.ts b/src/lib/models/Guild.ts index 7902461..bc93951 100644 --- a/src/lib/models/Guild.ts +++ b/src/lib/models/Guild.ts @@ -1,3 +1,4 @@ +import { Snowflake } from 'discord.js'; import { DataTypes, Optional, Sequelize } from 'sequelize'; import { BushClient } from '../extensions/BushClient'; import { BaseModel } from './BaseModel'; @@ -5,12 +6,17 @@ import { BaseModel } from './BaseModel'; export interface GuildModel { id: string; prefix: string; + autoPublishChannels: string[]; + blacklistedChannels: Snowflake[]; } -export type GuildModelCreationAttributes = Optional<GuildModel, 'prefix'>; + +export type GuildModelCreationAttributes = Optional<GuildModel, 'prefix' | 'autoPublishChannels' | 'blacklistedChannels'>; export class Guild extends BaseModel<GuildModel, GuildModelCreationAttributes> implements GuildModel { id: string; prefix: string; + autoPublishChannels: string[]; + blacklistedChannels: Snowflake[]; static initModel(seqeulize: Sequelize, client: BushClient): void { Guild.init( { @@ -22,6 +28,14 @@ export class Guild extends BaseModel<GuildModel, GuildModelCreationAttributes> i type: DataTypes.STRING, allowNull: false, defaultValue: client.config.prefix + }, + autoPublishChannels: { + type: DataTypes.ARRAY(DataTypes.STRING), + allowNull: true + }, + blacklistedChannels: { + type: DataTypes.ARRAY(DataTypes.STRING), + allowNull: true } }, { sequelize: seqeulize } diff --git a/src/lib/models/Level.ts b/src/lib/models/Level.ts index 426ec1a..6113627 100644 --- a/src/lib/models/Level.ts +++ b/src/lib/models/Level.ts @@ -32,7 +32,7 @@ export class Level extends BaseModel<LevelModel, LevelModelCreationAttributes> { defaultValue: 0 } }, - { sequelize: sequelize } + { sequelize } ); } static convertXpToLevel(xp: number): number { diff --git a/src/lib/models/Modlog.ts b/src/lib/models/Modlog.ts index 5a2cb69..15c5030 100644 --- a/src/lib/models/Modlog.ts +++ b/src/lib/models/Modlog.ts @@ -77,7 +77,7 @@ export class Modlog extends BaseModel<ModlogModel, ModlogModelCreationAttributes // } } }, - { sequelize: sequelize } + { sequelize } ); } } diff --git a/src/lib/models/StickyRole.ts b/src/lib/models/StickyRole.ts new file mode 100644 index 0000000..597d7c5 --- /dev/null +++ b/src/lib/models/StickyRole.ts @@ -0,0 +1,40 @@ +import { Snowflake } from 'discord.js'; +import { DataTypes, Sequelize } from 'sequelize'; +import { BaseModel } from './BaseModel'; + +export interface StickyRoleModel { + user: Snowflake; + guild: Snowflake; + roles: Snowflake[]; +} +export interface StickyRoleModelCreationAttributes { + user: Snowflake; + guild: Snowflake; + roles: Snowflake[]; +} + +export class StickyRole extends BaseModel<StickyRoleModel, StickyRoleModelCreationAttributes> implements StickyRoleModel { + user: Snowflake; + guild: Snowflake; + roles: Snowflake[]; + static initModel(sequelize: Sequelize): void { + StickyRole.init( + { + user: { + type: DataTypes.STRING, + allowNull: false + }, + guild: { + type: DataTypes.STRING, + allowNull: false + }, + + roles: { + type: DataTypes.ARRAY(DataTypes.STRING), + allowNull: false + } + }, + { sequelize } + ); + } +} diff --git a/src/lib/models/index.ts b/src/lib/models/index.ts index 8eb817e..e38ad69 100644 --- a/src/lib/models/index.ts +++ b/src/lib/models/index.ts @@ -1,5 +1,7 @@ +export * from './Ban'; export * from './BaseModel'; +export * from './Global'; export * from './Guild'; -export * from './Ban'; -export * from './Modlog'; export * from './Level'; +export * from './Modlog'; +export * from './StickyRole'; diff --git a/src/lib/utils/BushConstants.ts b/src/lib/utils/BushConstants.ts index 701f818..1015229 100644 --- a/src/lib/utils/BushConstants.ts +++ b/src/lib/utils/BushConstants.ts @@ -311,7 +311,8 @@ export class BushConstants { GUILD: 'guild', DM: 'dm', AUTHOR_NOT_FOUND: 'authorNotFound', - DISABLED: 'disabled', + DISABLED_GUILD: 'disabledGuild', + DISABLED_GLOBAL: 'disabledGlobal', ROLE_BLACKLIST: 'roleBlacklist', USER_BLACKLIST: 'userBlacklist', RESTRICTED_GUILD: 'restrictedGuild', @@ -340,4 +341,32 @@ export class BushConstants { SLASH_NOT_FOUND: 'slashNotFound', SLASH_MISSING_PERMISSIONS: 'slashMissingPermissions' }; + + public static moulberryBushRoleMap = [ + { name: '*', id: '792453550768390194' }, + { name: 'Admin Perms', id: '746541309853958186' }, + { name: 'Sr. Moderator', id: '782803470205190164' }, + { name: 'Moderator', id: '737308259823910992' }, + { name: 'Helper', id: '737440116230062091' }, + { name: 'Trial Helper', id: '783537091946479636' }, + { name: 'Contributor', id: '694431057532944425' }, + { name: 'Giveaway Donor', id: '784212110263451649' }, + { name: 'Giveaway (200m)', id: '810267756426690601' }, + { name: 'Giveaway (100m)', id: '801444430522613802' }, + { name: 'Giveaway (50m)', id: '787497512981757982' }, + { name: 'Giveaway (25m)', id: '787497515771232267' }, + { name: 'Giveaway (10m)', id: '787497518241153025' }, + { name: 'Giveaway (5m)', id: '787497519768403989' }, + { name: 'Giveaway (1m)', id: '787497521084891166' }, + { name: 'Suggester', id: '811922322767609877' }, + { name: 'Partner', id: '767324547312779274' }, + { name: 'Level Locked', id: '784248899044769792' }, + { name: 'No Files', id: '786421005039173633' }, + { name: 'No Reactions', id: '786421270924361789' }, + { name: 'No Links', id: '786421269356740658' }, + { name: 'No Bots', id: '786804858765312030' }, + { name: 'No VC', id: '788850482554208267' }, + { name: 'No Giveaways', id: '808265422334984203' }, + { name: 'No Support', id: '790247359824396319' } + ]; } diff --git a/src/lib/utils/BushLogger.ts b/src/lib/utils/BushLogger.ts index d48ec07..2225bde 100644 --- a/src/lib/utils/BushLogger.ts +++ b/src/lib/utils/BushLogger.ts @@ -85,7 +85,7 @@ export class BushLogger { */ public debug(...content: any): void { if (!this.client.config.dev) return; - console.log(`${chalk.bgGrey(this.getTimeStamp())} ${chalk.grey('[Debug]')}`, ...content); + console.log(`${chalk.bgMagenta(this.getTimeStamp())} ${chalk.magenta('[Debug]')}`, ...content); } /** @@ -102,7 +102,7 @@ export class BushLogger { ); if (!sendChannel) return; const embed = new MessageEmbed() - .setDescription(`**[${header}]** ${this.stripColor(newContent)}`) + .setDescription(`**[${header}]** ${this.parseFormatting(this.stripColor(newContent), '', true)}`) .setColor(this.client.util.colors.gray) .setTimestamp(); this.channelLog({ embeds: [embed] }); @@ -163,7 +163,7 @@ export class BushLogger { ); if (!sendChannel) return; const embed = new MessageEmbed() - .setDescription(`**[${header}]** ${this.stripColor(newContent)}`) + .setDescription(`**[${header}]** ${this.parseFormatting(this.stripColor(newContent), '', true)}`) .setColor(this.client.util.colors.error) .setTimestamp(); this.channelError({ embeds: [embed] }); @@ -183,7 +183,7 @@ export class BushLogger { ); if (!sendChannel) return; const embed = new MessageEmbed() - .setDescription(`**[${header}]** ${this.stripColor(newContent)}`) + .setDescription(`**[${header}]** ${this.parseFormatting(this.stripColor(newContent), '', true)}`) .setColor(this.client.util.colors.success) .setTimestamp(); await this.channelLog({ embeds: [embed] }).catch(() => {}); diff --git a/src/lib/utils/CanvasProgressBar.ts b/src/lib/utils/CanvasProgressBar.ts new file mode 100644 index 0000000..aa8630a --- /dev/null +++ b/src/lib/utils/CanvasProgressBar.ts @@ -0,0 +1,78 @@ +// 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 percentage: number; + private p: number; + private ctx: CanvasRenderingContext2D; + + constructor( + ctx: CanvasRenderingContext2D, + dimension: { x: number; y: number; width: number; height: number }, + color: string, + percentage: number + ) { + ({ x: this.x, y: this.y, width: this.w, height: this.h } = dimension); + this.color = color; + this.percentage = percentage; + this.p; + this.ctx = ctx; + } + + draw(): void { + // ----------------- + this.p = this.percentage * this.w; + if (this.p <= this.h) { + this.ctx.beginPath(); + this.ctx.arc( + this.h / 2 + this.x, + this.h / 2 + this.y, + this.h / 2, + Math.PI - Math.acos((this.h - this.p) / this.h), + Math.PI + Math.acos((this.h - this.p) / this.h) + ); + this.ctx.save(); + this.ctx.scale(-1, 1); + this.ctx.arc( + this.h / 2 - this.p - this.x, + this.h / 2 + this.y, + this.h / 2, + Math.PI - Math.acos((this.h - this.p) / this.h), + Math.PI + Math.acos((this.h - this.p) / this.h) + ); + this.ctx.restore(); + this.ctx.closePath(); + } else { + this.ctx.beginPath(); + this.ctx.arc(this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, Math.PI / 2, (3 / 2) * Math.PI); + this.ctx.lineTo(this.p - this.h + this.x, 0 + this.y); + this.ctx.arc(this.p - this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, (3 / 2) * Math.PI, Math.PI / 2); + this.ctx.lineTo(this.h / 2 + this.x, this.h + this.y); + this.ctx.closePath(); + } + this.ctx.fillStyle = this.color; + this.ctx.fill(); + } + + // showWholeProgressBar(){ + // this.ctx.beginPath(); + // this.ctx.arc(this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, Math.PI / 2, 3 / 2 * Math.PI); + // this.ctx.lineTo(this.w - this.h + this.x, 0 + this.y); + // this.ctx.arc(this.w - this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, 3 / 2 *Math.PI, Math.PI / 2); + // this.ctx.lineTo(this.h / 2 + this.x, this.h + this.y); + // this.ctx.strokeStyle = '#000000'; + // this.ctx.stroke(); + // this.ctx.closePath(); + // } + + get PPercentage(): number { + return this.percentage * 100; + } + + set PPercentage(x: number) { + this.percentage = x / 100; + } +} diff --git a/src/listeners/commands/commandBlocked.ts b/src/listeners/commands/commandBlocked.ts index 61433a6..febfc93 100644 --- a/src/listeners/commands/commandBlocked.ts +++ b/src/listeners/commands/commandBlocked.ts @@ -1,6 +1,6 @@ -import { Command } from 'discord-akairo'; -import { Message } from 'discord.js'; +import { BushCommand } from '../../lib/extensions/BushCommand'; import { BushListener } from '../../lib/extensions/BushListener'; +import { BushMessage } from '../../lib/extensions/BushMessage'; export default class CommandBlockedListener extends BushListener { public constructor() { @@ -10,24 +10,70 @@ export default class CommandBlockedListener extends BushListener { }); } - public async exec(message: Message, command: Command, reason: string): Promise<void> { + public async exec(message: BushMessage, command: BushCommand, reason: string): Promise<unknown> { this.client.console.info( 'CommandBlocked', `<<${message.author.tag}>> tried to run <<${message.util.parsed.command}>> but was blocked because <<${reason}>>.`, false ); + const reasons = this.client.consts.BlockedReasons; switch (reason) { - case 'owner': { - await message.util.send(`You must be an owner to run command \`${message.util.parsed.command}\``); - break; + case reasons.OWNER: { + return await message.util.reply({ + content: `${this.client.util.emojis.error} Only my developers can run the \`${message.util.parsed.command}\` command.` + }); } - case 'blacklist': { - // pass - break; + case reasons.SUPER_USER: { + return await message.util.reply({ + content: `${this.client.util.emojis.error} You must be a superuser to run the \`${message.util.parsed.command}\` command.` + }); + } + case reasons.DISABLED_GLOBAL: { + return await message.util.reply({ + content: `${this.client.util.emojis.error} My developers disabled the \`${message.util.parsed.command}\` command.` + }); + } + case reasons.DISABLED_GUILD: { + return await message.util.reply({ + content: `${this.client.util.emojis.error} The \`${command.aliases[0]}\` command is currently disabled in \`${message.guild.name}\`.` + }); + } + case reasons.CHANNEL_BLACKLIST: { + return; + } + case reasons.USER_BLACKLIST: { + return; + } + case reasons.ROLE_BLACKLIST: { + return; + } + case reasons.RESTRICTED_CHANNEL: { + const channels = command.restrictedChannels; + const names = []; + channels.forEach((c) => { + names.push(`<#${c}>`); + }); + const pretty = this.client.util.oxford(names, 'and', undefined); + return await message.util.reply({ + content: `${this.client.util.emojis.error} \`${command}\` can only be run in ${pretty}.` + }); + } + case reasons.RESTRICTED_GUILD: { + const guilds = command.restrictedGuilds; + const names = []; + guilds.forEach((g) => { + names.push(`\`${this.client.guilds.cache.get(g).name}\``); + }); + const pretty = this.client.util.oxford(names, 'and', undefined); + return await message.util.reply({ + content: `${this.client.util.emojis.error} \`${command}\` can only be run in ${pretty}.` + }); } default: { - await message.util.send(`Command blocked with reason \`${reason}\``); + return await message.util.reply({ + content: `${this.client.util.emojis.error} Command blocked with reason \`${reason}\`` + }); } } } diff --git a/src/listeners/commands/commandError.ts b/src/listeners/commands/commandError.ts index cb8c5d2..7f765ae 100644 --- a/src/listeners/commands/commandError.ts +++ b/src/listeners/commands/commandError.ts @@ -19,7 +19,7 @@ export default class CommandErrorListener extends BushListener { .setDescription( stripIndents`**User:** ${message.author} (${message.author.tag}) **Command:** ${command} - **Channel:** ${message.channel} (${message.channel.id}) + **Channel:** ${message.channel} (${message.channel?.id}) **Message:** [link](${message.url})` ) .addField('Error', await this.client.util.codeblock(`${error?.stack}`, 1024, 'js')) @@ -59,7 +59,8 @@ export default class CommandErrorListener extends BushListener { this.client.console.error( 'CommandError', `an error occurred with the <<${command}>> command in <<${channel}>> triggered by <<${message?.author?.tag}>>:\n` + - error?.stack + error?.stack, + false ); } } diff --git a/src/listeners/commands/commandStarted.ts b/src/listeners/commands/commandStarted.ts index 28ed0f8..8c860f8 100644 --- a/src/listeners/commands/commandStarted.ts +++ b/src/listeners/commands/commandStarted.ts @@ -4,7 +4,7 @@ import { BushListener } from '../../lib/extensions/BushListener'; export default class CommandStartedListener extends BushListener { constructor() { - super('logCommands', { + super('commandStarted', { emitter: 'commandHandler', event: 'commandStarted' }); diff --git a/src/listeners/commands/slashBlocked.ts b/src/listeners/commands/slashBlocked.ts new file mode 100644 index 0000000..d8ef736 --- /dev/null +++ b/src/listeners/commands/slashBlocked.ts @@ -0,0 +1,81 @@ +import { BushCommand } from '../../lib/extensions/BushCommand'; +import { BushInteractionMessage } from '../../lib/extensions/BushInteractionMessage'; +import { BushListener } from '../../lib/extensions/BushListener'; + +export default class SlashBlockedListener extends BushListener { + public constructor() { + super('slashBlocked', { + emitter: 'commandHandler', + event: 'slashBlocked' + }); + } + + public async exec(message: BushInteractionMessage, command: BushCommand, reason: string): Promise<unknown> { + this.client.console.info( + 'SlashBlocked', + `<<${message.author.tag}>> tried to run <<${message.util.parsed.command}>> but was blocked because <<${reason}>>.`, + false + ); + + const reasons = this.client.consts.BlockedReasons; + + switch (reason) { + case reasons.OWNER: { + return await message.util.reply({ + content: `${this.client.util.emojis.error} Only my developers can run the \`${message.util.parsed.command}\` command.` + }); + } + case reasons.SUPER_USER: { + return await message.util.reply({ + content: `${this.client.util.emojis.error} You must be a superuser to run the \`${message.util.parsed.command}\` command.` + }); + } + case reasons.DISABLED_GLOBAL: { + return await message.util.reply({ + content: `${this.client.util.emojis.error} My developers disabled the \`${message.util.parsed.command}\` command.` + }); + } + case reasons.DISABLED_GUILD: { + return await message.util.reply({ + content: `${this.client.util.emojis.error} The \`${command.aliases[0]}\` command is currently disabled in \`${message.guild.name}\`.` + }); + } + case reasons.CHANNEL_BLACKLIST: { + return; + } + case reasons.USER_BLACKLIST: { + return; + } + case reasons.ROLE_BLACKLIST: { + return; + } + case reasons.RESTRICTED_CHANNEL: { + const channels = command.restrictedChannels; + const names = []; + channels.forEach((c) => { + names.push(`<#${c}>`); + }); + const pretty = this.client.util.oxford(names, 'and', undefined); + return await message.util.reply({ + content: `${this.client.util.emojis.error} \`${command}\` can only be run in ${pretty}.` + }); + } + case reasons.RESTRICTED_GUILD: { + const guilds = command.restrictedGuilds; + const names = []; + guilds.forEach((g) => { + names.push(`\`${this.client.guilds.cache.get(g).name}\``); + }); + const pretty = this.client.util.oxford(names, 'and', undefined); + return await message.util.reply({ + content: `${this.client.util.emojis.error} \`${command}\` can only be run in ${pretty}.` + }); + } + default: { + return await message.util.reply({ + content: `${this.client.util.emojis.error} Command blocked with reason \`${reason}\`` + }); + } + } + } +} diff --git a/src/listeners/commands/slashCommandError.ts b/src/listeners/commands/slashCommandError.ts index b8d9b6b..718e992 100644 --- a/src/listeners/commands/slashCommandError.ts +++ b/src/listeners/commands/slashCommandError.ts @@ -1,6 +1,7 @@ import { stripIndents } from 'common-tags'; -import { CommandInteraction, MessageEmbed, TextChannel } from 'discord.js'; +import { MessageEmbed } from 'discord.js'; import { BushCommand } from '../../lib/extensions/BushCommand'; +import { BushInteractionMessage } from '../../lib/extensions/BushInteractionMessage'; import { BushListener } from '../../lib/extensions/BushListener'; export default class SlashCommandErrorListener extends BushListener { @@ -10,31 +11,55 @@ export default class SlashCommandErrorListener extends BushListener { event: 'slashError' }); } - async exec(error: Error, message: CommandInteraction, command: BushCommand): Promise<void> { - const errorNumber = Math.floor(Math.random() * 6969696969) + 69; // hehe funy numbers - const errorDevEmbed = this.client.util - .createEmbed(this.client.util.colors.error) - .setTitle(`Slash Error # \`${errorNumber}\`: An error occurred`) + async exec(error: Error, message: BushInteractionMessage, command: BushCommand): Promise<void> { + const errorNo = Math.floor(Math.random() * 6969696969) + 69; // hehe funny number + const errorEmbed: MessageEmbed = new MessageEmbed() + .setTitle(`Slash Error # \`${errorNo}\`: An error occurred`) .setDescription( - stripIndents`**User:** <@${message.user.id}> (${message.user.tag}) - **Slash Command:** ${command} - **Channel:** <#${message.channelID}> (${message.channelID}) - **Message:** [link](https://discord.com/${message.guildID}/${message.channelID}/${message.id})` + 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})` ) - .addField('Error', `${await this.client.util.haste(error.stack)}`); - let errorUserEmbed: MessageEmbed; - if (command) { - errorUserEmbed = this.client.util - .createEmbed(this.client.util.colors.error) - .setTitle('An error occurred') - .setDescription( - stripIndents`Whoops! It appears like something broke. - The developers have been notified of this. If you contact them, give them code \`${errorNumber}\`. - ` - ); + .addField('Error', await this.client.util.codeblock(`${error?.stack}`, 1024, 'js')) + .setColor(this.client.util.colors.error) + .setTimestamp(); + + if (message) { + if (!this.client.config.owners.includes(message.author.id)) { + const errorUserEmbed: MessageEmbed = new MessageEmbed() + .setTitle('An 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 + errorUserEmbed.setDescription( + `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') + .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 = (await this.client.channels.fetch(this.client.config.channels.log)) as TextChannel; - await channel.send({ embeds: [errorDevEmbed] }); - if (errorUserEmbed) await message.reply({ embeds: [errorUserEmbed] }); + const channel = message.channel.type === 'dm' ? message.channel.recipient.tag : message.channel.name; + this.client.console.error( + 'SlashError', + `an error occurred with the <<${command}>> command in <<${channel}>> triggered by <<${message?.author?.tag}>>:\n` + + error?.stack, + false + ); } } diff --git a/src/listeners/commands/slashStarted.ts b/src/listeners/commands/slashStarted.ts new file mode 100644 index 0000000..6a45546 --- /dev/null +++ b/src/listeners/commands/slashStarted.ts @@ -0,0 +1,21 @@ +import { Message } from 'discord.js'; +import { BushCommand } from '../../lib/extensions/BushCommand'; +import { BushListener } from '../../lib/extensions/BushListener'; + +export default class SlashStartedListener extends BushListener { + constructor() { + super('slashStarted', { + emitter: 'commandHandler', + event: 'slashStarted' + }); + } + exec(message: Message, 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}>>` + }.`, + false // I don't want to spam the log channel when people use commands + ); + } +} |