diff options
Diffstat (limited to 'src/lib/extensions')
-rw-r--r-- | src/lib/extensions/BushClient.ts | 78 | ||||
-rw-r--r-- | src/lib/extensions/BushClientUtil.ts | 879 | ||||
-rw-r--r-- | src/lib/extensions/BushCommand.ts | 35 | ||||
-rw-r--r-- | src/lib/extensions/BushCommandHandler.ts | 274 | ||||
-rw-r--r-- | src/lib/extensions/BushInhibitor.ts | 2 | ||||
-rw-r--r-- | src/lib/extensions/BushInhinitorHandler.ts | 6 | ||||
-rw-r--r-- | src/lib/extensions/BushListener.ts | 2 | ||||
-rw-r--r-- | src/lib/extensions/BushTask.ts | 2 | ||||
-rw-r--r-- | src/lib/extensions/BushTaskHandler.ts | 1 | ||||
-rw-r--r-- | src/lib/extensions/Util.ts | 598 |
10 files changed, 1242 insertions, 635 deletions
diff --git a/src/lib/extensions/BushClient.ts b/src/lib/extensions/BushClient.ts index e2e889b..1bd3493 100644 --- a/src/lib/extensions/BushClient.ts +++ b/src/lib/extensions/BushClient.ts @@ -1,26 +1,30 @@ import chalk from 'chalk'; -import { AkairoClient, InhibitorHandler, ListenerHandler, TaskHandler } from 'discord-akairo'; -import { Guild, Intents, Snowflake } from 'discord.js'; +import { AkairoClient, TaskHandler } from 'discord-akairo'; +import { APIMessage, Guild, Intents, Message, MessageOptions, Snowflake, UserResolvable } from 'discord.js'; import * as path from 'path'; import { exit } from 'process'; import { Sequelize } from 'sequelize'; import * as config from '../../config/options'; import * as Models from '../models'; import AllowedMentions from '../utils/AllowedMentions'; -import { BushLogger } from '../utils/Logger'; +import { BushCache } from '../utils/BushCache'; +import { BushLogger } from '../utils/BushLogger'; +import { BushClientUtil } from './BushClientUtil'; import { BushCommandHandler } from './BushCommandHandler'; -import { BushUtil } from './Util'; +import { BushInhibitorHandler } from './BushInhinitorHandler'; +import { BushListenerHandler } from './BushListenerHandler'; export type BotConfig = typeof config; +export type BushMessageType = string | APIMessage | (MessageOptions & { split?: false }); export class BushClient extends AkairoClient { public config: BotConfig; - public listenerHandler: ListenerHandler; - public inhibitorHandler: InhibitorHandler; + public listenerHandler: BushListenerHandler; + public inhibitorHandler: BushInhibitorHandler; public commandHandler: BushCommandHandler; public taskHandler: TaskHandler; - public util: BushUtil; - public ownerID: Snowflake[]; + public declare util: BushClientUtil; + public declare ownerID: Snowflake[]; public db: Sequelize; public logger: BushLogger; constructor(config: BotConfig) { @@ -36,19 +40,19 @@ export class BushClient extends AkairoClient { ); // Set token - this.token = config.credentials.botToken; + this.token = config.credentials.token; // Set config this.config = config; // Create listener handler - this.listenerHandler = new ListenerHandler(this, { + this.listenerHandler = new BushListenerHandler(this, { directory: path.join(__dirname, '..', '..', 'listeners'), automateCategories: true }); // Create inhibitor handler - this.inhibitorHandler = new InhibitorHandler(this, { + this.inhibitorHandler = new BushInhibitorHandler(this, { directory: path.join(__dirname, '..', '..', 'inhibitors'), automateCategories: true }); @@ -72,18 +76,24 @@ export class BushClient extends AkairoClient { commandUtilLifetime: 3e5, argumentDefaults: { prompt: { - timeout: 'Timed out.', - ended: 'Too many tries.', - cancel: 'Canceled.', + start: 'Placeholder argument prompt. If you see this please tell the devs.', + retry: 'Placeholder failed argument prompt. If you see this please tell the devs.', + modifyStart: (_: Message, str: string): string => `${str}\n\n Type \`cancel\` to cancel the command`, + modifyRetry: (_: Message, str: string): string => + `${str.replace('{error}', this.util.emojis.error)}\n\n Type \`cancel\` to cancel the command`, + timeout: 'You took too long the command has been cancelled', + ended: 'You exceeded the maximum amount of tries the command has been cancelled', + cancel: 'The command has been cancelled', + retries: 3, time: 3e4 - } + }, + otherwise: '' }, ignorePermissions: this.config.owners, - ignoreCooldown: this.config.owners, - automateCategories: true + ignoreCooldown: this.config.owners }); - this.util = new BushUtil(this); + this.util = new BushClientUtil(this); this.db = new Sequelize(this.config.dev ? 'bushbot-dev' : 'bushbot', this.config.db.username, this.config.db.password, { dialect: 'postgres', host: this.config.db.host, @@ -93,6 +103,10 @@ export class BushClient extends AkairoClient { this.logger = new BushLogger(this); } + get console(): BushLogger { + return this.logger; + } + // Initialize everything private async _init(): Promise<void> { this.commandHandler.useListenerHandler(this.listenerHandler); @@ -112,9 +126,9 @@ export class BushClient extends AkairoClient { for (const loader of Object.keys(loaders)) { try { loaders[loader].loadAll(); - this.logger.log(chalk.green('Successfully loaded ' + chalk.cyan(loader) + '.')); + this.logger.success('Startup', `Successfully loaded <<${loader}>>.` + chalk.cyan() + '.', false); } catch (e) { - console.error(chalk.red('Unable to load loader ' + chalk.cyan(loader) + ' with error ' + e)); + this.logger.error('Startup', `Unable to load loader <<${loader}>> with error:\n${e?.stack}`, false); } } this.taskHandler.startAll(); @@ -122,12 +136,14 @@ export class BushClient extends AkairoClient { } public async dbPreInit(): Promise<void> { - await this.db.authenticate(); - Models.Guild.initModel(this.db, this); - Models.Modlog.initModel(this.db); - Models.Ban.initModel(this.db); - Models.Level.initModel(this.db); - await this.db.sync(); // Sync all tables to fix everything if updated + try { + await this.db.authenticate(); + Models.Guild.initModel(this.db, this); + Models.Modlog.initModel(this.db); + Models.Ban.initModel(this.db); + Models.Level.initModel(this.db); + await this.db.sync(); // Sync all tables to fix everything if updated + } catch (error) {} } public async start(): Promise<void> { @@ -135,7 +151,7 @@ export class BushClient extends AkairoClient { await this._init(); await this.login(this.token); } catch (e) { - console.error(chalk.red(e.stack)); + this.console.error('Start', chalk.red(e.stack), false); exit(2); } } @@ -146,4 +162,12 @@ export class BushClient extends AkairoClient { return this.login(this.token); } } + + public isOwner(user: UserResolvable): boolean { + return this.config.owners.includes(this.users.resolveID(user)); + } + public isSuperUser(user: UserResolvable): boolean { + const userID = this.users.resolveID(user); + return !!BushCache?.superUsers?.includes(userID) || this.config.owners.includes(userID); + } } diff --git a/src/lib/extensions/BushClientUtil.ts b/src/lib/extensions/BushClientUtil.ts new file mode 100644 index 0000000..9b87efd --- /dev/null +++ b/src/lib/extensions/BushClientUtil.ts @@ -0,0 +1,879 @@ +import { exec } from 'child_process'; +import { ClientUtil, Command } from 'discord-akairo'; +import { + APIInteractionDataResolvedChannel, + APIInteractionDataResolvedGuildMember, + APIMessage, + APIRole, + ApplicationCommandOptionType +} from 'discord-api-types'; +import { + ButtonInteraction, + CommandInteraction, + CommandInteractionOption, + Constants, + Guild, + GuildChannel, + GuildMember, + InteractionReplyOptions, + Message, + MessageActionRow, + MessageButton, + MessageComponentInteraction, + MessageEditOptions, + MessageEmbed, + MessageOptions, + Role, + Snowflake, + TextChannel, + User, + Util, + WebhookEditMessageOptions +} from 'discord.js'; +import got from 'got'; +import { promisify } from 'util'; +import { BushClient } from './BushClient'; +import { BushMessage } from './BushMessage'; + +interface hastebinRes { + key: string; +} + +export interface uuidRes { + uuid: string; + username: string; + username_history?: { username: string }[] | null; + textures: { + custom: boolean; + slim: boolean; + skin: { + url: string; + data: string; + }; + raw: { + value: string; + signature: string; + }; + }; + 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; + /** The hastebin urls used to post to hastebin, attempts to post in order */ + public hasteURLs: string[] = [ + 'https://hst.sh', + 'https://hasteb.in', + 'https://hastebin.com', + 'https://mystb.in', + 'https://haste.clicksminuteper.net', + 'https://paste.pythondiscord.com', + 'https://haste.unbelievaboat.com', + 'https://haste.tyman.tech' + ]; + public paginateEmojis = { + beginning: '853667381335162910', + back: '853667410203770881', + stop: '853667471110570034', + forward: '853667492680564747', + end: '853667514915225640' + }; + + /** A simple promise exec method */ + private exec = promisify(exec); + + /** + * Creates this client util + * @param client The client to initialize with + */ + constructor(client: BushClient) { + super(client); + } + + /** + * Maps an array of user ids to user objects. + * @param ids The list of IDs to map + * @returns The list of users mapped + */ + public async mapIDs(ids: Snowflake[]): Promise<User[]> { + return await Promise.all(ids.map((id) => this.client.users.fetch(id))); + } + + /** + * Capitalizes the first letter of the given text + * @param text The text to capitalize + * @returns The capitalized text + */ + public capitalize(text: string): string { + return text.charAt(0).toUpperCase() + text.slice(1); + } + + /** + * Runs a shell command and gives the output + * @param command The shell command to run + * @returns The stdout and stderr of the shell command + */ + public async shell(command: string): Promise<{ + stdout: string; + stderr: string; + }> { + return await this.exec(command); + } + + /** + * Posts text to hastebin + * @param content The text to post + * @returns The url of the posted text + */ + public async haste(content: string): Promise<string> { + for (const url of this.hasteURLs) { + try { + const res: hastebinRes = await got.post(`${url}/documents`, { body: content }).json(); + return `${url}/${res.key}`; + } catch (e) { + this.client.console.error('Haste', `Unable to upload haste to ${url}`); + continue; + } + } + return 'Unable to post'; + } + + /** + * Resolves a user-provided string into a user object, if possible + * @param text The text to try and resolve + * @returns The user resolved or null + */ + public async resolveUserAsync(text: string): Promise<User | null> { + const idReg = /\d{17,19}/; + const idMatch = text.match(idReg); + if (idMatch) { + try { + const user = await this.client.users.fetch(text as Snowflake); + return user; + } catch { + // pass + } + } + const mentionReg = /<@!?(?<id>\d{17,19})>/; + const mentionMatch = text.match(mentionReg); + if (mentionMatch) { + try { + const user = await this.client.users.fetch(mentionMatch.groups.id as Snowflake); + return user; + } catch { + // pass + } + } + const user = this.client.users.cache.find((u) => u.username === text); + if (user) return user; + return null; + } + + /** + * Appends the correct ordinal to the given number + * @param n The number to append an ordinal to + * @returns The number with the ordinal + */ + public ordinal(n: number): string { + const s = ['th', 'st', 'nd', 'rd'], + v = n % 100; + return n + (s[(v - 20) % 10] || s[v] || s[0]); + } + + /** + * Chunks an array to the specified size + * @param arr The array to chunk + * @param perChunk The amount of items per chunk + * @returns The chunked array + */ + public chunk<T>(arr: T[], perChunk: number): T[][] { + return arr.reduce((all, one, i) => { + const ch = Math.floor(i / perChunk); + all[ch] = [].concat(all[ch] || [], one); + return all; + }, []); + } + + /** Commonly Used Colors */ + public colors = { + default: '#1FD8F1', + error: '#EF4947', + warn: '#FEBA12', + success: '#3BB681', + info: '#3B78FF', + red: '#ff0000', + blue: '#0055ff', + aqua: '#00bbff', + purple: '#8400ff', + blurple: '#5440cd', + pink: '#ff00e6', + green: '#00ff1e', + darkGreen: '#008f11', + gold: '#b59400', + yellow: '#ffff00', + white: '#ffffff', + gray: '#a6a6a6', + lightGray: '#cfcfcf', + darkGray: '#7a7a7a', + black: '#000000', + orange: '#E86100' + }; + + /** Commonly Used Emojis */ + public emojis = { + success: '<:checkmark:837109864101707807>', + warn: '<:warn:848726900876247050> ', + error: '<:error:837123021016924261>', + successFull: '<:checkmark_full:850118767576088646>', + warnFull: '<:warn_full:850118767391539312>', + errorFull: '<:error_full:850118767295201350>', + mad: '<:mad:783046135392239626>', + join: '<:join:850198029809614858>', + leave: '<:leave:850198048205307919>', + loading: '<a:Loading:853419254619963392>' + }; + + /** + * A simple utility to create and embed with the needed style for the bot + */ + public createEmbed(color?: string, author?: User | GuildMember): MessageEmbed { + if (author instanceof GuildMember) { + author = author.user; // Convert to User if GuildMember + } + let embed = new MessageEmbed().setTimestamp(); + if (author) + embed = embed.setAuthor( + author.username, + author.displayAvatarURL({ dynamic: true }), + `https://discord.com/users/${author.id}` + ); + if (color) embed = embed.setColor(color); + return embed; + } + + public async mcUUID(username: string): Promise<string> { + const apiRes = (await got.get(`https://api.ashcon.app/mojang/v2/user/${username}`).json()) as uuidRes; + return apiRes.uuid.replace(/-/g, ''); + } + + public async syncSlashCommands(force = false, guild?: Snowflake): Promise<void> { + let fetchedGuild: Guild; + if (guild) fetchedGuild = this.client.guilds.cache.get(guild); + try { + const registered = + guild === undefined ? await this.client.application.commands.fetch() : await fetchedGuild.commands.fetch(); + for (const [, registeredCommand] of registered) { + if (!this.client.commandHandler.modules.find((cmd) => cmd.id == registeredCommand.name)?.execSlash || force) { + guild === undefined + ? await this.client.application.commands.delete(registeredCommand.id) + : await fetchedGuild.commands.delete(registeredCommand.id); + this.client.logger.verbose( + 'syncSlashCommands', + `Deleted slash command <<${registeredCommand.name}>>${ + guild !== undefined ? ` in guild <<${fetchedGuild.name}>>` : '' + }` + ); + } + } + + for (const [, botCommand] of this.client.commandHandler.modules) { + if (botCommand.execSlash) { + const found = registered.find((i) => i.name == botCommand.id); + Command; + const slashdata = { + name: botCommand.id, + description: botCommand.description.content, + options: botCommand.options.slashCommandOptions + }; + botCommand; + + if (found?.id && !force) { + if (slashdata.description !== found.description) { + guild === undefined + ? await this.client.application.commands.edit(found.id, slashdata) + : fetchedGuild.commands.edit(found.id, slashdata); + this.client.logger.verbose( + 'syncSlashCommands', + `Edited slash command <<${botCommand.id}>>${guild !== undefined ? ` in guild <<${fetchedGuild?.name}>>` : ''}` + ); + } + } else { + guild === undefined + ? await this.client.application.commands.create(slashdata) + : fetchedGuild.commands.create(slashdata); + this.client.logger.verbose( + 'syncSlashCommands', + `Created slash command <<${botCommand.id}>>${guild !== undefined ? ` in guild <<${fetchedGuild?.name}>>` : ''}}` + ); + } + } + } + + return this.client.logger.log( + 'syncSlashCommands', + `Slash commands registered${guild !== undefined ? ` in guild <<${fetchedGuild?.name}>>` : ''}` + ); + } catch (e) { + return this.client.logger.error( + 'syncSlashCommands', + `Slash commands not registered${ + guild !== undefined ? ` in guild <<${fetchedGuild?.name}>>` : '' + }, with the following error:\n${e?.stack}` + ); + } + } + + 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, + embeds: MessageEmbed[], + text: string | null = null, + deleteOnExit?: boolean + ): Promise<void> { + if (deleteOnExit === undefined) deleteOnExit = true; + + embeds.forEach((_e, i) => { + embeds[i] = embeds[i].setFooter(`Page ${i + 1}/${embeds.length}`); + }); + + const style = Constants.MessageButtonStyles.PRIMARY; + let curPage = 0; + if (typeof embeds !== 'object') throw 'embeds must be an object'; + const msg = (await message.util.reply({ + content: text, + embeds: [embeds[curPage]], + components: [getPaginationRow()] + })) as Message; + const filter = (interaction: ButtonInteraction) => + interaction.customID.startsWith('paginate_') && interaction.message == msg; + const collector = msg.createMessageComponentInteractionCollector(filter, { time: 300000 }); + collector.on('collect', async (interaction: MessageComponentInteraction) => { + if (interaction.user.id == message.author.id || this.client.config.owners.includes(interaction.user.id)) { + switch (interaction.customID) { + case 'paginate_beginning': { + curPage = 0; + await edit(interaction); + break; + } + case 'paginate_back': { + curPage--; + await edit(interaction); + break; + } + case 'paginate_stop': { + if (deleteOnExit) { + await interaction.deferUpdate().catch(() => {}); + if (msg.deletable && !msg.deleted) { + await msg.delete(); + } + } else { + await interaction + ?.update({ content: `${text ? text + '\n' : ''}Command closed by user.`, embeds: [], components: [] }) + .catch(() => {}); + } + return; + } + case 'paginate_next': { + curPage++; + await edit(interaction); + break; + } + case 'paginate_end': { + curPage = embeds.length - 1; + await edit(interaction); + break; + } + } + } else { + return await interaction?.deferUpdate().catch(() => {}); + } + }); + + collector.on('end', async () => { + await msg.edit({ content: text, embeds: [embeds[curPage]], components: [getPaginationRow(true)] }).catch(() => {}); + }); + + async function edit(interaction: MessageComponentInteraction): Promise<void> { + return await interaction + ?.update({ content: text, embeds: [embeds[curPage]], components: [getPaginationRow()] }) + .catch(() => {}); + } + const paginateEmojis = this.paginateEmojis; + function getPaginationRow(disableAll = false): MessageActionRow { + return new MessageActionRow().addComponents( + new MessageButton({ + style, + customID: 'paginate_beginning', + emoji: paginateEmojis.beginning, + disabled: disableAll || curPage == 0 + }), + new MessageButton({ + style, + customID: 'paginate_back', + emoji: paginateEmojis.back, + disabled: disableAll || curPage == 0 + }), + new MessageButton({ style, customID: 'paginate_stop', emoji: paginateEmojis.stop, disabled: disableAll }), + new MessageButton({ + style, + customID: 'paginate_next', + emoji: paginateEmojis.forward, + disabled: disableAll || curPage == embeds.length - 1 + }), + new MessageButton({ + style, + customID: 'paginate_end', + emoji: paginateEmojis.end, + disabled: disableAll || curPage == embeds.length - 1 + }) + ); + } + } + + /** Sends a message with a button for the user to delete it. */ + public async sendWithDeleteButton(message: BushMessage, options: MessageOptions): Promise<void> { + updateOptions(); + const msg = (await message.util.reply(options as MessageOptions & { split?: false })) as Message; + const filter = (interaction: ButtonInteraction) => interaction.customID == 'paginate__stop' && interaction.message == msg; + const collector = msg.createMessageComponentInteractionCollector(filter, { time: 300000 }); + collector.on('collect', async (interaction: MessageComponentInteraction) => { + if (interaction.user.id == message.author.id || this.client.config.owners.includes(interaction.user.id)) { + await interaction.deferUpdate().catch(() => {}); + if (msg.deletable && !msg.deleted) { + await msg.delete(); + } + return; + } else { + return await interaction?.deferUpdate().catch(() => {}); + } + }); + + collector.on('end', async () => { + updateOptions(true, true); + await msg.edit(options as MessageEditOptions).catch(() => {}); + }); + + const paginateEmojis = this.paginateEmojis; + function updateOptions(edit?: boolean, disable?: boolean) { + if (edit == undefined) edit = false; + if (disable == undefined) disable = false; + options.components = [ + new MessageActionRow().addComponents( + new MessageButton({ + style: Constants.MessageButtonStyles.PRIMARY, + customID: 'paginate__stop', + emoji: paginateEmojis.stop, + disabled: disable + }) + ) + ]; + if (edit) { + options.reply = undefined; + } + } + } + + /** + * Surrounds text in a code block with the specified language and puts it in a hastebin if its too long. + * + * * Embed Description Limit = 2048 characters + * * Embed Field Limit = 1024 characters + */ + public async codeblock(code: string, length: number, language: 'ts' | 'js' | 'sh' | 'json' | '' = ''): Promise<string> { + let hasteOut = ''; + const tildes = '```'; + const formattingLength = 2 * tildes.length + language.length + 2 * '\n'.length; + if (code.length + formattingLength > length) hasteOut = 'Too large to display. Hastebin: ' + (await this.haste(code)); + + const code2 = code.length > length ? code.substring(0, length - (hasteOut.length + '\n'.length + formattingLength)) : code; + return ( + tildes + language + '\n' + Util.cleanCodeBlockContent(code2) + '\n' + tildes + (hasteOut.length ? '\n' + hasteOut : '') + ); + } + + public async slashRespond( + interaction: CommandInteraction, + responseOptions: string | InteractionReplyOptions + ): Promise<Message | APIMessage | void> { + let newResponseOptions: InteractionReplyOptions | WebhookEditMessageOptions = {}; + if (typeof responseOptions === 'string') { + newResponseOptions.content = responseOptions; + } else { + newResponseOptions = responseOptions; + } + if (interaction.replied || interaction.deferred) { + //@ts-expect-error: stop being dumb + delete newResponseOptions.ephemeral; // Cannot change a preexisting message to be ephemeral + return (await interaction.editReply(newResponseOptions)) as APIMessage; + } else { + await interaction.reply(newResponseOptions); + return await interaction.fetchReply().catch(() => {}); + } + } + + /** 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>; + } + + /** A bunch of mappings */ + public mappings = { + guilds: { + bush: '516977525906341928', + tree: '767448775450820639', + staff: '784597260465995796', + space_ship: '717176538717749358', + sbr: '839287012409999391' + }, + + permissions: { + CREATE_INSTANT_INVITE: { name: 'Create Invite', important: false }, + KICK_MEMBERS: { name: 'Kick Members', important: true }, + BAN_MEMBERS: { name: 'Ban Members', important: true }, + ADMINISTRATOR: { name: 'Administrator', important: true }, + MANAGE_CHANNELS: { name: 'Manage Channels', important: true }, + MANAGE_GUILD: { name: 'Manage Server', important: true }, + ADD_REACTIONS: { name: 'Add Reactions', important: false }, + VIEW_AUDIT_LOG: { name: 'View Audit Log', important: true }, + PRIORITY_SPEAKER: { name: 'Priority Speaker', important: true }, + STREAM: { name: 'Video', important: false }, + VIEW_CHANNEL: { name: 'View Channel', important: false }, + SEND_MESSAGES: { name: 'Send Messages', important: false }, + SEND_TTS_MESSAGES: { name: 'Send Text-to-Speech Messages', important: true }, + MANAGE_MESSAGES: { name: 'Manage Messages', important: true }, + EMBED_LINKS: { name: 'Embed Links', important: false }, + ATTACH_FILES: { name: 'Attach Files', important: false }, + READ_MESSAGE_HISTORY: { name: 'Read Message History', important: false }, + MENTION_EVERYONE: { name: 'Mention @everyone, @here, and All Roles', important: true }, // name has a zero-width space to prevent accidents + USE_EXTERNAL_EMOJIS: { name: 'Use External Emoji', important: false }, + VIEW_GUILD_INSIGHTS: { name: 'View Server Insights', important: true }, + CONNECT: { name: 'Connect', important: false }, + SPEAK: { name: 'Speak', important: false }, + MUTE_MEMBERS: { name: 'Mute Members', important: true }, + DEAFEN_MEMBERS: { name: 'Deafen Members', important: true }, + MOVE_MEMBERS: { name: 'Move Members', important: true }, + USE_VAD: { name: 'Use Voice Activity', important: false }, + CHANGE_NICKNAME: { name: 'Change Nickname', important: false }, + MANAGE_NICKNAMES: { name: 'Change Nicknames', important: true }, + MANAGE_ROLES: { name: 'Manage Roles', important: true }, + MANAGE_WEBHOOKS: { name: 'Manage Webhooks', important: true }, + MANAGE_EMOJIS: { name: 'Manage Emojis', important: true }, + USE_APPLICATION_COMMANDS: { name: 'Use Slash Commands', important: false }, + REQUEST_TO_SPEAK: { name: 'Request to Speak', important: false }, + USE_PUBLIC_THREADS: { name: 'Use Public Threads', important: false }, + USE_PRIVATE_THREADS: { name: 'Use Private Threads', important: true } + }, + + features: { + ANIMATED_ICON: { name: 'Animated Icon', important: false, emoji: '<:animatedIcon:850774498071412746>', weight: 14 }, + BANNER: { name: 'Banner', important: false, emoji: '<:banner:850786673150787614>', weight: 15 }, + COMMERCE: { name: 'Store Channels', important: true, emoji: '<:storeChannels:850786692432396338>', weight: 11 }, + COMMUNITY: { name: 'Community', important: false, emoji: '<:community:850786714271875094>', weight: 20 }, + DISCOVERABLE: { name: 'Discoverable', important: true, emoji: '<:discoverable:850786735360966656>', weight: 6 }, + ENABLED_DISCOVERABLE_BEFORE: { + name: 'Enabled Discovery Before', + important: false, + emoji: '<:enabledDiscoverableBefore:850786754670624828>', + weight: 7 + }, + FEATURABLE: { name: 'Featurable', important: true, emoji: '<:featurable:850786776372084756>', weight: 4 }, + INVITE_SPLASH: { name: 'Invite Splash', important: false, emoji: '<:inviteSplash:850786798246559754>', weight: 16 }, + MEMBER_VERIFICATION_GATE_ENABLED: { + name: 'Membership Verification Gate', + important: false, + emoji: '<:memberVerificationGateEnabled:850786829984858212>', + weight: 18 + }, + MONETIZATION_ENABLED: { name: 'Monetization Enabled', important: true, emoji: null, weight: 8 }, + MORE_EMOJI: { name: 'More Emoji', important: true, emoji: '<:moreEmoji:850786853497602080>', weight: 3 }, + MORE_STICKERS: { name: 'More Stickers', important: true, emoji: null, weight: 2 }, + NEWS: { + name: 'Announcement Channels', + important: false, + emoji: '<:announcementChannels:850790491796013067>', + weight: 17 + }, + PARTNERED: { name: 'Partnered', important: true, emoji: '<:partneredServer:850794851955507240>', weight: 1 }, + PREVIEW_ENABLED: { name: 'Preview Enabled', important: true, emoji: '<:previewEnabled:850790508266913823>', weight: 10 }, + RELAY_ENABLED: { name: 'Relay Enabled', important: true, emoji: '<:relayEnabled:850790531441229834>', weight: 5 }, + TICKETED_EVENTS_ENABLED: { name: 'Ticketed Events Enabled', important: true, emoji: null, weight: 9 }, + VANITY_URL: { name: 'Vanity URL', important: false, emoji: '<:vanityURL:850790553079644160>', weight: 12 }, + VERIFIED: { name: 'Verified', important: true, emoji: '<:verified:850795049817473066>', weight: 0 }, + VIP_REGIONS: { name: 'VIP Regions', important: false, emoji: '<:VIPRegions:850794697496854538>', weight: 13 }, + WELCOME_SCREEN_ENABLED: { + name: 'Welcome Screen Enabled', + important: false, + emoji: '<:welcomeScreenEnabled:850790575875817504>', + weight: 19 + } + }, + + otherEmojis: { + SERVER_BOOSTER_1: '<:serverBooster1:848740052091142145>', + SERVER_BOOSTER_2: '<:serverBooster2:848740090506510388>', + SERVER_BOOSTER_3: '<:serverBooster3:848740124992077835>', + SERVER_BOOSTER_6: '<:serverBooster6:848740155245461514>', + SERVER_BOOSTER_9: '<:serverBooster9:848740188846030889>', + SERVER_BOOSTER_12: '<:serverBooster12:848740304365551668>', + SERVER_BOOSTER_15: '<:serverBooster15:848740354890137680>', + SERVER_BOOSTER_18: '<:serverBooster18:848740402886606868>', + SERVER_BOOSTER_24: '<:serverBooster24:848740444628320256>', + NITRO: '<:nitro:848740498054971432>', + BOOSTER: '<:booster:848747775020892200>', + OWNER: '<:owner:848746439311753286>', + ADMIN: '<:admin:848963914628333598>', + SUPERUSER: '<:superUser:848947986326224926>', + DEVELOPER: '<:developer:848954538111139871>', + BUSH_VERIFIED: '<:verfied:853360152090771497>', + BOOST_1: '<:boostitle:853363736679940127>', + BOOST_2: '<:boostitle:853363752728789075>', + BOOST_3: '<:boostitle:853363769132056627>', + TEXT: '<:text:853375537791893524>', + NEWS: '<:announcements:853375553531674644>', + VOICE: '<:voice:853375566735212584>', + STAGE: '<:stage:853375583521210468>', + STORE: '<:store:853375601175691266>', + CATEGORY: '<:category:853375615260819476>' + }, + + userFlags: { + DISCORD_EMPLOYEE: '<:discordEmployee:848742947826434079>', + PARTNERED_SERVER_OWNER: '<:partneredServerOwner:848743051593777152>', + HYPESQUAD_EVENTS: '<:hypeSquadEvents:848743108283072553>', + BUGHUNTER_LEVEL_1: '<:bugHunter:848743239850393640>', + HOUSE_BRAVERY: '<:hypeSquadBravery:848742910563844127>', + HOUSE_BRILLIANCE: '<:hypeSquadBrilliance:848742840649646101>', + HOUSE_BALANCE: '<:hypeSquadBalance:848742877537370133>', + EARLY_SUPPORTER: '<:earlySupporter:848741030102171648>', + //'TEAM_USER': '', + //'SYSTEM': '', + BUGHUNTER_LEVEL_2: '<:bugHunterGold:848743283080822794>', + //'VERIFIED_BOT': '', + EARLY_VERIFIED_BOT_DEVELOPER: '<:earlyVerifiedBotDeveloper:848741079875846174>' + }, + + status: { + online: '<:online:848937141639577690>', + idle: '<:idle:848937158261211146>', + dnd: '<:dnd:848937173780135986>', + offline: '<:offline:848939387277672448>', + streaming: '<:streaming:848937187479519242>' + }, + + maybeNitroDiscrims: ['1111', '2222', '3333', '4444', '5555', '6666', '6969', '7777', '8888', '9999'], + + capes: [ + // supporter capes + { name: 'patreon1', index: 0 }, + { name: 'patreon2', index: 1 }, + { name: 'fade', custom: 'https://raw.githubusercontent.com/NotEnoughUpdates/capes/master/fade.gif', index: 2 }, + { name: 'lava', custom: 'https://raw.githubusercontent.com/NotEnoughUpdates/capes/master/lava.gif', index: 3 }, + { + name: 'mcworld', + custom: 'https://raw.githubusercontent.com/NotEnoughUpdates/capes/master/mcworld_compressed.gif', + index: 4 + }, + { + name: 'negative', + custom: 'https://raw.githubusercontent.com/NotEnoughUpdates/capes/master/negative_compressed.gif', + index: 5 + }, + { + name: 'space', + custom: 'https://raw.githubusercontent.com/NotEnoughUpdates/capes/master/space_compressed.gif', + index: 6 + }, + { name: 'void', custom: 'https://raw.githubusercontent.com/NotEnoughUpdates/capes/master/void.gif', index: 7 }, + { name: 'tunnel', custom: 'https://raw.githubusercontent.com/NotEnoughUpdates/capes/master/tunnel.gif', index: 8 }, + // Staff capes + { name: 'contrib', index: 9 }, + { name: 'mbstaff', index: 10 }, + { name: 'ironmoon', index: 11 }, + { name: 'gravy', index: 12 }, + { name: 'nullzee', index: 13 }, + // partner capes + { name: 'thebakery', index: 14 }, + { name: 'dsm', index: 15 }, + { name: 'packshq', index: 16 }, + { name: 'furf', index: 17 }, + { name: 'skytils', index: 18 }, + { name: 'sbp', index: 19 }, + { name: 'subreddit_light', index: 20 }, + { name: 'subreddit_dark', index: 21 }, + // streamer capes + { name: 'alexxoffi', index: 22 }, + { name: 'jakethybro', index: 23 }, + { name: 'krusty', index: 24 }, + { name: 'soldier', index: 25 }, + { name: 'zera', index: 26 } + ], + roleMap: [ + { 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' }, + { name: 'DJ', id: '782619038403919902' } + ], + roleWhitelist: { + 'Partner': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'Suggester': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator', 'Helper', 'Trial Helper', 'Contributor'], + 'Level Locked': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'No Files': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'No Reactions': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'No Links': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'No Bots': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'No VC': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'No Giveaways': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator', 'Helper'], + 'No Support': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'Giveaway Donor': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'Giveaway (200m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'Giveaway (100m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'Giveaway (50m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'Giveaway (25m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'Giveaway (10m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'Giveaway (5m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'Giveaway (1m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], + 'DJ': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'] + } + }; +} + +// 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/lib/extensions/BushCommand.ts b/src/lib/extensions/BushCommand.ts index 2b34c69..9de2c95 100644 --- a/src/lib/extensions/BushCommand.ts +++ b/src/lib/extensions/BushCommand.ts @@ -1,8 +1,16 @@ +/* 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 { CommandInteraction, Snowflake } from 'discord.js'; import { BushClient } from './BushClient'; +import { BushInteractionMessage } from './BushInteractionMessage'; +import { BushMessage } from './BushMessage'; export interface BushCommandOptions extends CommandOptions { + hidden?: boolean; + restrictedChannels?: Snowflake[]; + restrictedGuilds?: Snowflake[]; slashCommandOptions?: APIApplicationCommandOption[]; description: { content: string; @@ -12,10 +20,33 @@ export interface BushCommandOptions extends CommandOptions { } export class BushCommand extends Command { - public client: BushClient; - options: BushCommandOptions; + public declare client: BushClient; + 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; + this.hidden = options.hidden || false; + this.restrictedChannels = options.restrictedChannels; + this.restrictedGuilds = options.restrictedGuilds; + } + + 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); + } + + /** Be careful when using this with a slash command since only the interaction is parsed as the message */ + public before(message: BushMessage): any; + public before(message: BushMessage | CommandInteraction): any; + public before(message) { + super.before(message); } } diff --git a/src/lib/extensions/BushCommandHandler.ts b/src/lib/extensions/BushCommandHandler.ts index 6ef44d7..670125d 100644 --- a/src/lib/extensions/BushCommandHandler.ts +++ b/src/lib/extensions/BushCommandHandler.ts @@ -1,15 +1,279 @@ -import { CommandHandler, CommandHandlerOptions } from 'discord-akairo'; -import { Collection } from 'discord.js'; +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { AkairoMessage, Category, CommandHandler, CommandHandlerOptions, CommandUtil, Util } from 'discord-akairo'; +import { Collection, CommandInteraction, GuildMember, Interaction } from 'discord.js'; import { BushClient } from './BushClient'; import { BushCommand } from './BushCommand'; +import { BushMessage } from './BushMessage'; -export interface BushCommandHandlerOptions extends CommandHandlerOptions {} +export const ArgumentMatches = { + PHRASE: 'phrase', + FLAG: 'flag', + OPTION: 'option', + REST: 'rest', + SEPARATE: 'separate', + TEXT: 'text', + CONTENT: 'content', + REST_CONTENT: 'restContent', + NONE: 'none' +}; +export const ArgumentTypes = { + STRING: 'string', + LOWERCASE: 'lowercase', + UPPERCASE: 'uppercase', + CHAR_CODES: 'charCodes', + NUMBER: 'number', + INTEGER: 'integer', + BIGINT: 'bigint', + EMOJINT: 'emojint', + URL: 'url', + DATE: 'date', + COLOR: 'color', + USER: 'user', + USERS: 'users', + MEMBER: 'member', + MEMBERS: 'members', + RELEVANT: 'relevant', + RELEVANTS: 'relevants', + CHANNEL: 'channel', + CHANNELS: 'channels', + TEXT_CHANNEL: 'textChannel', + TEXT_CHANNELS: 'textChannels', + VOICE_CHANNEL: 'voiceChannel', + VOICE_CHANNELS: 'voiceChannels', + CATEGORY_CHANNEL: 'categoryChannel', + CATEGORY_CHANNELS: 'categoryChannels', + NEWS_CHANNEL: 'newsChannel', + NEWS_CHANNELS: 'newsChannels', + STORE_CHANNEL: 'storeChannel', + STORE_CHANNELS: 'storeChannels', + ROLE: 'role', + ROLES: 'roles', + EMOJI: 'emoji', + EMOJIS: 'emojis', + GUILD: 'guild', + GUILDS: 'guilds', + MESSAGE: 'message', + GUILD_MESSAGE: 'guildMessage', + RELEVANT_MESSAGE: 'relevantMessage', + INVITE: 'invite', + MEMBER_MENTION: 'memberMention', + CHANNEL_MENTION: 'channelMention', + ROLE_MENTION: 'roleMention', + EMOJI_MENTION: 'emojiMention', + COMMAND_ALIAS: 'commandAlias', + COMMAND: 'command', + INHIBITOR: 'inhibitor', + LISTENER: 'listener' +}; + +export const blockedReasons = { + DM: 'dm', + BOT: 'bot', + GUILD: 'guild', + OWNER: 'owner', + CLIENT: 'client', + DISABLED: 'disabled', + SUPERUSER: 'superuser', + ROLE_BLACKLIST: 'roleBlacklist', + USER_BLACKLIST: 'userBlacklist', + RESTRICTED_GUILD: 'restrictedGuild', + CHANNEL_BLACKLIST: 'channelBlacklist', + RESTRICTED_CHANNEL: 'restrictedChannel' +}; + +export const CommandHandlerEvents = { + MESSAGE_BLOCKED: 'messageBlocked', + MESSAGE_INVALID: 'messageInvalid', + COMMAND_BLOCKED: 'commandBlocked', + COMMAND_STARTED: 'commandStarted', + COMMAND_FINISHED: 'commandFinished', + COMMAND_CANCELLED: 'commandCancelled', + COMMAND_LOCKED: 'commandLocked', + COMMAND_INVALID: 'commandInvalid', + COMMAND_LOCKED_NSFW: 'commandLockedNsfw', + MISSING_PERMISSIONS: 'missingPermissions', + COOLDOWN: 'cooldown', + IN_PROMPT: 'inPrompt', + ERROR: 'error', + SLASH_COMMAND_BLOCKED: 'slashCommandBlocked' +}; + +// A large amount of this code is copied from Akairo so that I can add custom checks to it. export class BushCommandHandler extends CommandHandler { - public constructor(client: BushClient, options: BushCommandHandlerOptions) { + public declare client: BushClient; + public declare modules: Collection<string, BushCommand>; + public declare categories: Collection<string, Category<string, BushCommand>>; + public constructor(client: BushClient, options: CommandHandlerOptions) { super(client, options); this.client = client; } - declare modules: Collection<string, BushCommand>; + async handleSlash(interaction: Interaction): Promise<boolean> { + if (!interaction.isCommand()) return false; + + if (await this.runAllTypeInhibitors(interaction)) { + return false; + } + + if (!interaction.guildID) { + this.emit('slashGuildOnly', interaction); + return false; + } + const command = this.findCommand(interaction.commandName) as BushCommand; + const before = command.before(interaction); + if (Util.isPromise(before)) await before; + + if (!command) { + this.emit('slashNotFound', interaction); + return false; + } + + if (command.ownerOnly && !this.client.isOwner(interaction.user)) { + this.emit('slashBlocked', interaction, command, 'owner'); + return false; + } + if (command.superUserOnly && !this.client.isSuperUser(interaction.user)) { + this.emit('slashBlocked', interaction, command, 'superuser'); + return false; + } + + if (interaction.channel.type !== 'dm') { + const userPermissions = interaction.channel.permissionsFor(interaction.member as GuildMember).toArray(); + + if (command.userPermissions) { + const userMissingPermissions = + typeof command.userPermissions === 'object' + ? command.userPermissions.filter((p) => !userPermissions.includes(p)) + : ''; + if (command.userPermissions && command.userPermissions.length > 0 && userMissingPermissions.length > 0) { + this.emit('slashMissingPermissions', interaction, command, 'user', userMissingPermissions); + return false; + } + } + + const clientPermissions = interaction.channel.permissionsFor(interaction.guild.me).toArray(); + + if (command.clientPermissions) { + const clientMissingPermissions = command.clientPermissions.filter((p) => !clientPermissions.includes(p)); + if (command.clientPermissions && command.clientPermissions.length > 0 && clientMissingPermissions.length > 0) { + this.emit('slashMissingPermissions', interaction, command, 'client', clientMissingPermissions); + return false; + } + } + } + + //@ts-ignore: Typings are wrong + if (this.runCooldowns(interaction, command)) { + return true; + } + const message = new AkairoMessage(this.client, interaction, { + slash: true, + replied: this.autoDefer || command.slashEphemeral + }); + + if (this.commandUtil) { + if (this.commandUtils.has(message.id)) { + message.util = this.commandUtils.get(message.id); + } else { + message.util = new CommandUtil(this, message); + this.commandUtils.set(message.id, message.util); + } + } + + let parsed = await this.parseCommand(message); + if (!parsed.command) { + const overParsed = await this.parseCommandOverwrittenPrefixes(message); + if (overParsed.command || (parsed.prefix == null && overParsed.prefix != null)) { + parsed = overParsed; + } + } + + if (this.commandUtil) { + message.util.parsed = parsed; + } + + try { + if (this.autoDefer || command.slashEphemeral) { + await interaction.defer(command.slashEphemeral); + } + const convertedOptions = {}; + for (const option of interaction.options.values()) { + if (option.member) convertedOptions[option.name] = option.member; + else if (option.channel) convertedOptions[option.name] = option.channel; + else if (option.role) convertedOptions[option.name] = option.role; + else convertedOptions[option.name] = option.value; + } + this.emit('slashStarted', interaction, command); + + if (command.execSlash || this.execSlash) await command.execSlash(message, convertedOptions); + else await command.exec(message, convertedOptions); + + return true; + } catch (err) { + this.emit('slashError', err, message, command); + return false; + } + } + public async runPostTypeInhibitors(message: BushMessage, command: BushCommand): Promise<boolean> { + if (command.ownerOnly && !message.client.isOwner(message.author)) { + super.emit(CommandHandlerEvents.COMMAND_BLOCKED, message, command, blockedReasons.OWNER); + return true; + } + if (command.superUserOnly && !(this.client.isSuperUser(message.author) || this.client.isOwner(message.author))) { + super.emit(CommandHandlerEvents.COMMAND_BLOCKED, message, command, blockedReasons.SUPERUSER); + return true; + } + if (command.channel === 'guild' && !message.guild) { + this.emit(CommandHandlerEvents.COMMAND_BLOCKED, message, command, blockedReasons.GUILD); + return true; + } + if (command.channel === 'dm' && message.guild) { + this.emit(CommandHandlerEvents.COMMAND_BLOCKED, message, command, blockedReasons.DM); + return true; + } + if (command.restrictedChannels?.length && message.channel) { + if (!command.restrictedChannels.includes(message.channel.id)) { + this.emit(CommandHandlerEvents.COMMAND_BLOCKED, message, command, blockedReasons.RESTRICTED_CHANNEL); + return true; + } + } + if (command.restrictedGuilds?.length && message.guild) { + if (!command.restrictedGuilds.includes(message.guild.id)) { + this.emit(CommandHandlerEvents.COMMAND_BLOCKED, message, command, blockedReasons.RESTRICTED_GUILD); + return true; + } + } + if (await this.runPermissionChecks(message, command)) { + return true; + } + const reason = this.inhibitorHandler ? await this.inhibitorHandler.test('post', message, command) : null; + if (reason != null) { + this.emit(CommandHandlerEvents.COMMAND_BLOCKED, message, command, reason); + return true; + } + if (this.runCooldowns(message, command)) { + return true; + } + return false; + } + public async runCommand(message: BushMessage, command: BushCommand, args: unknown): Promise<void> { + if (command.typing) { + message.channel.startTyping(); + } + try { + this.emit(CommandHandlerEvents.COMMAND_STARTED, message, command, args); + const ret = await command.exec(message, args); + this.emit(CommandHandlerEvents.COMMAND_FINISHED, message, command, args, ret); + } finally { + if (command.typing) { + message.channel.stopTyping(); + } + } + } + public runAllTypeInhibitors(message: BushMessage): any; + public runAllTypeInhibitors(message: BushMessage | CommandInteraction): any; + public runAllTypeInhibitors(message): any { + super.runAllTypeInhibitors(message); + } } diff --git a/src/lib/extensions/BushInhibitor.ts b/src/lib/extensions/BushInhibitor.ts index d9a9b68..85d6de8 100644 --- a/src/lib/extensions/BushInhibitor.ts +++ b/src/lib/extensions/BushInhibitor.ts @@ -2,5 +2,5 @@ import { Inhibitor } from 'discord-akairo'; import { BushClient } from './BushClient'; export class BushInhibitor extends Inhibitor { - public client: BushClient; + public declare client: BushClient; } diff --git a/src/lib/extensions/BushInhinitorHandler.ts b/src/lib/extensions/BushInhinitorHandler.ts new file mode 100644 index 0000000..2a947da --- /dev/null +++ b/src/lib/extensions/BushInhinitorHandler.ts @@ -0,0 +1,6 @@ +import { InhibitorHandler } from 'discord-akairo'; +import { BushClient } from './BushClient'; + +export class BushInhibitorHandler extends InhibitorHandler { + public declare client: BushClient; +} diff --git a/src/lib/extensions/BushListener.ts b/src/lib/extensions/BushListener.ts index 6a13210..e555e89 100644 --- a/src/lib/extensions/BushListener.ts +++ b/src/lib/extensions/BushListener.ts @@ -2,5 +2,5 @@ import { Listener } from 'discord-akairo'; import { BushClient } from './BushClient'; export class BushListener extends Listener { - public client: BushClient; + public declare client: BushClient; } diff --git a/src/lib/extensions/BushTask.ts b/src/lib/extensions/BushTask.ts index 21655e9..06d0602 100644 --- a/src/lib/extensions/BushTask.ts +++ b/src/lib/extensions/BushTask.ts @@ -2,5 +2,5 @@ import { Task } from 'discord-akairo'; import { BushClient } from './BushClient'; export class BushTask extends Task { - public client: BushClient; + public declare client: BushClient; } diff --git a/src/lib/extensions/BushTaskHandler.ts b/src/lib/extensions/BushTaskHandler.ts index f783eb3..c76dbfb 100644 --- a/src/lib/extensions/BushTaskHandler.ts +++ b/src/lib/extensions/BushTaskHandler.ts @@ -1,6 +1,7 @@ import { AkairoHandlerOptions, TaskHandler } from 'discord-akairo'; import { BushClient } from './BushClient'; +// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface BushTaskHandlerOptions extends AkairoHandlerOptions {} export class BushTaskHandler extends TaskHandler { diff --git a/src/lib/extensions/Util.ts b/src/lib/extensions/Util.ts deleted file mode 100644 index 3913437..0000000 --- a/src/lib/extensions/Util.ts +++ /dev/null @@ -1,598 +0,0 @@ -import chalk from 'chalk'; -import { exec } from 'child_process'; -import { ClientUtil, Command } from 'discord-akairo'; -import { - APIInteractionDataResolvedChannel, - APIInteractionDataResolvedGuildMember, - APIRole, - ApplicationCommandOptionType -} from 'discord-api-types'; -import { - ButtonInteraction, - CommandInteractionOption, - Constants, - Guild, - GuildChannel, - GuildMember, - MessageActionRow, - MessageButton, - MessageComponentInteraction, - MessageEmbed, - MessageOptions, - Role, - Snowflake, - User, - Util -} from 'discord.js'; -import got from 'got'; -import { promisify } from 'util'; -import { BushClient } from './BushClient'; -import { BushMessage } from './BushMessage'; - -interface hastebinRes { - key: string; -} - -export interface uuidRes { - uuid: string; - username: string; - username_history?: { username: string }[] | null; - textures: { - custom: boolean; - slim: boolean; - skin: { - url: string; - data: string; - }; - raw: { - value: string; - signature: string; - }; - }; - 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 BushUtil extends ClientUtil { - /** - * The client of this ClientUtil - * @type {BushClient} - */ - public client: BushClient; - /** - * The hastebin urls used to post to hastebin, attempts to post in order - * @type {string[]} - */ - public hasteURLs = [ - 'https://hst.sh', - 'https://hasteb.in', - 'https://hastebin.com', - 'https://mystb.in', - 'https://haste.clicksminuteper.net', - 'https://paste.pythondiscord.com', - 'https://haste.unbelievaboat.com', - 'https://haste.tyman.tech' - ]; - /** - * A simple promise exec method - */ - private exec = promisify(exec); - - /** - * Creates this client util - * @param client The client to initialize with - */ - constructor(client: BushClient) { - super(client); - } - - /** - * Maps an array of user ids to user objects. - * @param ids The list of IDs to map - * @returns The list of users mapped - */ - public async mapIDs(ids: Snowflake[]): Promise<User[]> { - return await Promise.all(ids.map((id) => this.client.users.fetch(id))); - } - - /** - * Capitalizes the first letter of the given text - * @param text The text to capitalize - * @returns The capitalized text - */ - public capitalize(text: string): string { - return text.charAt(0).toUpperCase() + text.slice(1); - } - - /** - * Runs a shell command and gives the output - * @param command The shell command to run - * @returns The stdout and stderr of the shell command - */ - public async shell(command: string): Promise<{ - stdout: string; - stderr: string; - }> { - return await this.exec(command); - } - - /** - * Posts text to hastebin - * @param content The text to post - * @returns The url of the posted text - */ - public async haste(content: string): Promise<string> { - for (const url of this.hasteURLs) { - try { - const res: hastebinRes = await got.post(`${url}/documents`, { body: content }).json(); - return `${url}/${res.key}`; - } catch (e) { - // pass - } - } - throw new Error('No urls worked. (wtf)'); - } - - /** - * Resolves a user-provided string into a user object, if possible - * @param text The text to try and resolve - * @returns The user resolved or null - */ - public async resolveUserAsync(text: string): Promise<User | null> { - const idReg = /\d{17,19}/; - const idMatch = text.match(idReg); - if (idMatch) { - try { - const user = await this.client.users.fetch(text as Snowflake); - return user; - } catch { - // pass - } - } - const mentionReg = /<@!?(?<id>\d{17,19})>/; - const mentionMatch = text.match(mentionReg); - if (mentionMatch) { - try { - const user = await this.client.users.fetch(mentionMatch.groups.id as Snowflake); - return user; - } catch { - // pass - } - } - const user = this.client.users.cache.find((u) => u.username === text); - if (user) return user; - return null; - } - - /** - * Appends the correct ordinal to the given number - * @param n The number to append an ordinal to - * @returns The number with the ordinal - */ - public ordinal(n: number): string { - const s = ['th', 'st', 'nd', 'rd'], - v = n % 100; - return n + (s[(v - 20) % 10] || s[v] || s[0]); - } - - /** - * Chunks an array to the specified size - * @param arr The array to chunk - * @param perChunk The amount of items per chunk - * @returns The chunked array - */ - public chunk<T>(arr: T[], perChunk: number): T[][] { - return arr.reduce((all, one, i) => { - const ch = Math.floor(i / perChunk); - all[ch] = [].concat(all[ch] || [], one); - return all; - }, []); - } - - /** - * The colors used throught the bot - */ - public colors = { - default: '#1FD8F1', - error: '#EF4947', - warn: '#FEBA12', - success: '#3BB681', - red: '#ff0000', - orange: '#E86100', - gold: '#b59400', - yellow: '#ffff00', - green: '#00ff1e', - darkGreen: '#008f11', - aqua: '#00bbff', - blue: '#0055ff', - blurple: '#5440cd', - purple: '#8400ff', - pink: '#ff00e6', - white: '#ffffff', - gray: '#a6a6a6', - lightGray: '#cfcfcf', - darkGray: '#7a7a7a', - black: '#000000' - }; - - public emojis = { - success: '<:checkmark:837109864101707807>', - warn: '<:warn:848726900876247050> ', - error: '<:error:837123021016924261>', - successFull: '<:checkmark_full:850118767576088646>', - warnFull: '<:warn_full:850118767391539312>', - errorFull: '<:error_full:850118767295201350>', - mad: '<:mad:783046135392239626>', - join: '<:join:850198029809614858>', - leave: '<:leave:850198048205307919>' - }; - - /** - * A simple utility to create and embed with the needed style for the bot - */ - public createEmbed(color?: string, author?: User | GuildMember): MessageEmbed { - if (author instanceof GuildMember) { - author = author.user; // Convert to User if GuildMember - } - let embed = new MessageEmbed().setTimestamp(); - if (author) - embed = embed.setAuthor( - author.username, - author.displayAvatarURL({ dynamic: true }), - `https://discord.com/users/${author.id}` - ); - if (color) embed = embed.setColor(color); - return embed; - } - - public async mcUUID(username: string): Promise<string> { - const apiRes = (await got.get(`https://api.ashcon.app/mojang/v2/user/${username}`).json()) as uuidRes; - return apiRes.uuid.replace(/-/g, ''); - } - - public async syncSlashCommands(force = false, guild?: Snowflake): Promise<void> { - let fetchedGuild: Guild; - if (guild) fetchedGuild = this.client.guilds.cache.get(guild); - try { - const registered = - guild === undefined ? await this.client.application.commands.fetch() : await fetchedGuild.commands.fetch(); - for (const [, registeredCommand] of registered) { - if (!this.client.commandHandler.modules.find((cmd) => cmd.id == registeredCommand.name)?.execSlash || force) { - guild === undefined - ? await this.client.application.commands.delete(registeredCommand.id) - : await fetchedGuild.commands.delete(registeredCommand.id); - this.client.logger.verbose( - chalk`{red Deleted slash command ${registeredCommand.name}${ - guild !== undefined ? ` in guild ${fetchedGuild.name}` : '' - }}` - ); - } - } - - for (const [, botCommand] of this.client.commandHandler.modules) { - if (botCommand.execSlash) { - const found = registered.find((i) => i.name == botCommand.id); - Command; - const slashdata = { - name: botCommand.id, - description: botCommand.description.content, - options: botCommand.options.slashCommandOptions - }; - botCommand; - - if (found?.id && !force) { - if (slashdata.description !== found.description) { - guild === undefined - ? await this.client.application.commands.edit(found.id, slashdata) - : fetchedGuild.commands.edit(found.id, slashdata); - this.client.logger.verbose( - chalk`{yellow Edited slash command ${botCommand.id}${ - guild !== undefined ? ` in guild ${fetchedGuild.name}` : '' - }}` - ); - } - } else { - guild === undefined - ? await this.client.application.commands.create(slashdata) - : fetchedGuild.commands.create(slashdata); - this.client.logger.verbose( - chalk`{green Created slash command ${botCommand.id}${ - guild !== undefined ? ` in guild ${fetchedGuild.name}` : '' - }}` - ); - } - } - } - - return this.client.logger.log( - chalk.green(`Slash commands registered${guild !== undefined ? ` in guild ${fetchedGuild.name}` : ''}`) - ); - } catch (e) { - console.log(chalk.red(e.stack)); - return this.client.logger.error( - chalk`{red Slash commands not registered${ - guild !== undefined ? ` in guild ${fetchedGuild.name}` : '' - }, see above error.}` - ); - } - } - - 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' } - ]; - - private paginateEmojis = { - begging: '853667381335162910', - back: '853667410203770881', - stop: '853667471110570034', - forward: '853667492680564747', - end: '853667514915225640' - }; - - public async buttonPaginate( - message: BushMessage, - embeds: MessageEmbed[], - text: string | null = null, - deleteOnExit?: boolean - ): Promise<void> { - if (deleteOnExit === undefined) deleteOnExit = true; - embeds.forEach((_e, i) => { - embeds[i] = embeds[i].setFooter(`Page ${i + 1}/${embeds.length}`); - }); - - const style = Constants.MessageButtonStyles.PRIMARY; - let curPage = 0; - if (typeof embeds !== 'object') throw 'embeds must be an object'; - const msg = await message.util.reply({ content: text, embeds: [embeds[curPage]], components: [getPaginationRow()] }); - const filter = (interaction: ButtonInteraction) => - interaction.customID.startsWith('paginate_') && interaction.message == msg; - const collector = msg.createMessageComponentInteractionCollector(filter, { time: 300000 }); - collector.on('collect', async (interaction: MessageComponentInteraction) => { - if (interaction.user.id == message.author.id || message.client.config.owners.includes(interaction.user.id)) { - switch (interaction.customID) { - case 'paginate_beginning': { - curPage = 0; - await edit(interaction); - break; - } - case 'paginate_back': { - curPage--; - await edit(interaction); - break; - } - case 'paginate_stop': { - if (deleteOnExit) { - await interaction.deferUpdate(); - await msg.delete(); - } else { - await interaction?.update({ - content: `${text ? text + '\n' : ''}Command closed by user.`, - embeds: [], - components: [] - }); - } - return; - } - case 'paginate_next': { - curPage++; - await edit(interaction); - break; - } - case 'paginate_end': { - curPage = embeds.length - 1; - await edit(interaction); - break; - } - } - } else { - return await interaction?.deferUpdate(); - } - }); - - collector.on('end', async () => { - await msg.edit({ content: text, embeds: [embeds[curPage]], components: [getPaginationRow(true)] }).catch(() => {}); - }); - - async function edit(interaction: MessageComponentInteraction): Promise<void> { - return await interaction?.update({ content: text, embeds: [embeds[curPage]], components: [getPaginationRow()] }); - } - function getPaginationRow(disableAll = false): MessageActionRow { - return new MessageActionRow().addComponents( - new MessageButton({ - style, - customID: 'paginate_beginning', - emoji: this.paginateEmojis.begging, - disabled: disableAll || curPage == 0 - }), - new MessageButton({ - style, - customID: 'paginate_back', - emoji: this.paginateEmojis.back, - disabled: disableAll || curPage == 0 - }), - new MessageButton({ style, customID: 'paginate_stop', emoji: this.paginateEmojis.stop, disabled: disableAll }), - new MessageButton({ - style, - customID: 'paginate_next', - emoji: this.paginateEmojis.forward, - disabled: disableAll || curPage == embeds.length - 1 - }), - new MessageButton({ - style, - customID: 'paginate_end', - emoji: this.paginateEmojis.end, - disabled: disableAll || curPage == embeds.length - 1 - }) - ); - } - } - - public async sendWithDeleteButton(message: BushMessage, options: MessageOptions): Promise<void> { - updateOptions(); - const msg = await message.util.reply(options as MessageOptions & { split?: false }); - const filter = (interaction: ButtonInteraction) => interaction.customID == 'paginate__stop' && interaction.message == msg; - const collector = msg.createMessageComponentInteractionCollector(filter, { time: 300000 }); - collector.on('collect', async (interaction: MessageComponentInteraction) => { - if (interaction.user.id == message.author.id || message.client.config.owners.includes(interaction.user.id)) { - await interaction.deferUpdate(); - await msg.delete(); - return; - } else { - return await interaction?.deferUpdate(); - } - }); - - collector.on('end', async () => { - updateOptions(true, true); - await msg.edit(options); - }); - - function updateOptions(edit?: boolean, disable?: boolean) { - if (edit == undefined) edit = false; - if (disable == undefined) disable = false; - options.components = [ - new MessageActionRow().addComponents( - new MessageButton({ - style: Constants.MessageButtonStyles.PRIMARY, - customID: 'paginate__stop', - emoji: this.paginateEmojis.stop, - disabled: disable - }) - ) - ]; - if (edit) { - options.reply = undefined; - } - } - } - /** - * Surrounds text in a code block with the specified language and puts it in a haste bin if it too long. - * - * * Embed Description Limit = 2048 characters - * * Embed Field Limit = 1024 characters - */ - public async codeblock(code: string, length: number, language: 'ts' | 'js' | 'sh' | 'json' | '' = ''): Promise<string> { - let hasteOut = ''; - const tildes = '```'; - const formattingLength = 2 * tildes.length + language.length + 2 * '\n'.length; - if (code.length + formattingLength > length) hasteOut = 'Too large to display. Hastebin: ' + (await this.haste(code)); - - const code2 = code.length > length ? code.substring(0, length - (hasteOut.length + '\n'.length + formattingLength)) : code; - return ( - tildes + language + '\n' + Util.cleanCodeBlockContent(code2) + '\n' + tildes + (hasteOut.length ? '\n' + hasteOut : '') - ); - } -} - -// 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() { - // ----------------- - 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() { - return this.percentage * 100; - } - - set PPercentage(x) { - this.percentage = x / 100; - } -} |