diff options
author | IRONM00N <64110067+IRONM00N@users.noreply.github.com> | 2022-08-18 22:42:12 -0400 |
---|---|---|
committer | IRONM00N <64110067+IRONM00N@users.noreply.github.com> | 2022-08-18 22:42:12 -0400 |
commit | 2356d2c44736fb83021dacb551625852111c8ce6 (patch) | |
tree | 10408d22fdd7a358d2f5c5917c3b59e55aa4c19d /src/lib/utils | |
parent | 8aed6f93f7740c592cbc0e2f9fd3269c05286077 (diff) | |
download | tanzanite-2356d2c44736fb83021dacb551625852111c8ce6.tar.gz tanzanite-2356d2c44736fb83021dacb551625852111c8ce6.tar.bz2 tanzanite-2356d2c44736fb83021dacb551625852111c8ce6.zip |
restructure, experimental presence and member automod, fixed bugs probably made some more bugs
Diffstat (limited to 'src/lib/utils')
-rw-r--r-- | src/lib/utils/AllowedMentions.ts | 68 | ||||
-rw-r--r-- | src/lib/utils/BushCache.ts | 26 | ||||
-rw-r--r-- | src/lib/utils/BushClientUtils.ts | 498 | ||||
-rw-r--r-- | src/lib/utils/BushConstants.ts | 531 | ||||
-rw-r--r-- | src/lib/utils/BushLogger.ts | 315 | ||||
-rw-r--r-- | src/lib/utils/BushUtils.ts | 612 | ||||
-rw-r--r-- | src/lib/utils/CanvasProgressBar.ts | 83 |
7 files changed, 0 insertions, 2133 deletions
diff --git a/src/lib/utils/AllowedMentions.ts b/src/lib/utils/AllowedMentions.ts deleted file mode 100644 index d2eb030..0000000 --- a/src/lib/utils/AllowedMentions.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { type MessageMentionOptions, type MessageMentionTypes } from 'discord.js'; - -/** - * A utility class for creating allowed mentions. - */ -export class AllowedMentions { - /** - * @param everyone Whether everyone and here should be mentioned. - * @param roles Whether roles should be mentioned. - * @param users Whether users should be mentioned. - * @param repliedUser Whether the author of the Message being replied to should be mentioned. - */ - public constructor(public everyone = false, public roles = false, public users = true, public repliedUser = true) {} - - /** - * Don't mention anyone. - * @param repliedUser Whether the author of the Message being replied to should be mentioned. - */ - public static none(repliedUser = true): MessageMentionOptions { - return { parse: [], repliedUser }; - } - - /** - * Mention @everyone and @here, roles, and users. - * @param repliedUser Whether the author of the Message being replied to should be mentioned. - */ - public static all(repliedUser = true): MessageMentionOptions { - return { parse: ['everyone', 'roles', 'users'], repliedUser }; - } - - /** - * Mention users. - * @param repliedUser Whether the author of the Message being replied to should be mentioned. - */ - public static users(repliedUser = true): MessageMentionOptions { - return { parse: ['users'], repliedUser }; - } - - /** - * Mention everyone and here. - * @param repliedUser Whether the author of the Message being replied to should be mentioned. - */ - public static everyone(repliedUser = true): MessageMentionOptions { - return { parse: ['everyone'], repliedUser }; - } - - /** - * Mention roles. - * @param repliedUser Whether the author of the Message being replied to should be mentioned. - */ - public static roles(repliedUser = true): MessageMentionOptions { - return { parse: ['roles'], repliedUser }; - } - - /** - * Converts this into a MessageMentionOptions object. - */ - public toObject(): MessageMentionOptions { - return { - parse: [ - ...(this.users ? ['users'] : []), - ...(this.roles ? ['roles'] : []), - ...(this.everyone ? ['everyone'] : []) - ] as MessageMentionTypes[], - repliedUser: this.repliedUser - }; - } -} diff --git a/src/lib/utils/BushCache.ts b/src/lib/utils/BushCache.ts deleted file mode 100644 index 22a13ef..0000000 --- a/src/lib/utils/BushCache.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { BadWords, GlobalModel, SharedModel, type Guild } from '#lib'; -import { Collection, type Snowflake } from 'discord.js'; - -export class BushCache { - public global = new GlobalCache(); - public shared = new SharedCache(); - public guilds = new GuildCache(); -} - -export class GlobalCache implements Omit<GlobalModel, 'environment'> { - public disabledCommands: string[] = []; - public blacklistedChannels: Snowflake[] = []; - public blacklistedGuilds: Snowflake[] = []; - public blacklistedUsers: Snowflake[] = []; -} - -export class SharedCache implements Omit<SharedModel, 'primaryKey'> { - public superUsers: Snowflake[] = []; - public privilegedUsers: Snowflake[] = []; - public badLinksSecret: string[] = []; - public badLinks: string[] = []; - public badWords: BadWords = {}; - public autoBanCode: string | null = null; -} - -export class GuildCache extends Collection<Snowflake, Guild> {} diff --git a/src/lib/utils/BushClientUtils.ts b/src/lib/utils/BushClientUtils.ts deleted file mode 100644 index 920ff40..0000000 --- a/src/lib/utils/BushClientUtils.ts +++ /dev/null @@ -1,498 +0,0 @@ -import assert from 'assert/strict'; -import { - cleanCodeBlockContent, - DMChannel, - escapeCodeBlock, - GuildMember, - Message, - PartialDMChannel, - Routes, - TextBasedChannel, - ThreadMember, - User, - type APIMessage, - type Client, - type Snowflake, - type UserResolvable -} from 'discord.js'; -import got from 'got'; -import _ from 'lodash'; -import { ConfigChannelKey } from '../../../config/Config.js'; -import CommandErrorListener from '../../listeners/commands/commandError.js'; -import { BushInspectOptions } from '../common/typings/BushInspectOptions.js'; -import { CodeBlockLang } from '../common/typings/CodeBlockLang.js'; -import { CommandMessage } from '../extensions/discord-akairo/BushCommand.js'; -import { SlashMessage } from '../extensions/discord-akairo/SlashMessage.js'; -import { Global } from '../models/shared/Global.js'; -import { Shared } from '../models/shared/Shared.js'; -import { GlobalCache, SharedCache } from './BushCache.js'; -import { emojis, Pronoun, PronounCode, pronounMapping, regex } from './BushConstants.js'; -import { addOrRemoveFromArray, formatError, inspect } from './BushUtils.js'; - -/** - * Utilities that require access to the client. - */ -export class BushClientUtils { - /** - * The hastebin urls used to post to hastebin, attempts to post in order - */ - #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 constructor(private readonly client: 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))); - } - - /** - * Posts text to hastebin - * @param content The text to post - * @returns The url of the posted text - */ - public async haste(content: string, substr = false): Promise<HasteResults> { - let isSubstr = false; - if (content.length > 400_000 && !substr) { - void this.handleError('haste', new Error(`content over 400,000 characters (${content.length.toLocaleString()})`)); - return { error: 'content too long' }; - } else if (content.length > 400_000) { - content = content.substring(0, 400_000); - isSubstr = true; - } - for (const url of this.#hasteURLs) { - try { - const res: HastebinRes = await got.post(`${url}/documents`, { body: content }).json(); - return { url: `${url}/${res.key}`, error: isSubstr ? 'substr' : undefined }; - } catch { - void this.client.console.error('haste', `Unable to upload haste to ${url}`); - } - } - return { error: '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 { - return await this.client.users.fetch(text as Snowflake); - } catch {} - } - const mentionReg = /<@!?(?<id>\d{17,19})>/; - const mentionMatch = text.match(mentionReg); - if (mentionMatch) { - try { - return await this.client.users.fetch(mentionMatch.groups!.id as Snowflake); - } catch {} - } - const user = this.client.users.cache.find((u) => u.username === text); - if (user) return user; - return null; - } - - /** - * Surrounds text in a code block with the specified language and puts it in a hastebin if its too long. - * * Embed Description Limit = 4096 characters - * * Embed Field Limit = 1024 characters - * @param code The content of the code block. - * @param length The maximum length of the code block. - * @param language The language of the code. - * @param substr Whether or not to substring the code if it is too long. - * @returns The generated code block - */ - public async codeblock(code: string, length: number, language: CodeBlockLang | '' = '', substr = false): Promise<string> { - let hasteOut = ''; - code = escapeCodeBlock(code); - const prefix = `\`\`\`${language}\n`; - const suffix = '\n```'; - if (code.length + (prefix + suffix).length >= length) { - const haste_ = await this.haste(code, substr); - hasteOut = `Too large to display. ${ - haste_.url - ? `Hastebin: ${haste_.url}${language ? `.${language}` : ''}${haste_.error ? ` - ${haste_.error}` : ''}` - : `${emojis.error} Hastebin: ${haste_.error}` - }`; - } - - const FormattedHaste = hasteOut.length ? `\n${hasteOut}` : ''; - const shortenedCode = hasteOut ? code.substring(0, length - (prefix + FormattedHaste + suffix).length) : code; - const code3 = code.length ? prefix + shortenedCode + suffix + FormattedHaste : prefix + suffix; - if (code3.length > length) { - void this.client.console.warn(`codeblockError`, `Required Length: ${length}. Actual Length: ${code3.length}`, true); - void this.client.console.warn(`codeblockError`, code3, true); - throw new Error('code too long'); - } - return code3; - } - - /** - * Maps the key of a credential with a readable version when redacting. - * @param key The key of the credential. - * @returns The readable version of the key or the original key if there isn't a mapping. - */ - #mapCredential(key: string): string { - return ( - { - token: 'Main Token', - devToken: 'Dev Token', - betaToken: 'Beta Token', - hypixelApiKey: 'Hypixel Api Key', - wolframAlphaAppId: 'Wolfram|Alpha App ID', - dbPassword: 'Database Password' - }[key] ?? key - ); - } - - /** - * Redacts credentials from a string. - * @param text The text to redact credentials from. - * @returns The redacted text. - */ - public redact(text: string) { - for (const credentialName in { ...this.client.config.credentials, dbPassword: this.client.config.db.password }) { - const credential = { ...this.client.config.credentials, dbPassword: this.client.config.db.password }[ - credentialName as keyof typeof this.client.config.credentials - ]; - if (credential === null || credential === '') continue; - const replacement = this.#mapCredential(credentialName); - const escapeRegex = /[.*+?^${}()|[\]\\]/g; - text = text.replace(new RegExp(credential.toString().replace(escapeRegex, '\\$&'), 'g'), `[${replacement} Omitted]`); - text = text.replace( - new RegExp([...credential.toString()].reverse().join('').replace(escapeRegex, '\\$&'), 'g'), - `[${replacement} Omitted]` - ); - } - return text; - } - - /** - * Takes an any value, inspects it, redacts credentials, and puts it in a codeblock - * (and uploads to hast if the content is too long). - * @param input The object to be inspect, redacted, and put into a codeblock. - * @param language The language to make the codeblock. - * @param inspectOptions The options for {@link BushClientUtil.inspect}. - * @param length The maximum length that the codeblock can be. - * @returns The generated codeblock. - */ - public async inspectCleanRedactCodeblock( - input: any, - language?: CodeBlockLang | '', - inspectOptions?: BushInspectOptions, - length = 1024 - ) { - input = inspect(input, inspectOptions ?? undefined); - if (inspectOptions) inspectOptions.inspectStrings = undefined; - input = cleanCodeBlockContent(input); - input = this.redact(input); - return this.codeblock(input, length, language, true); - } - - /** - * Takes an any value, inspects it, redacts credentials, and uploads it to haste. - * @param input The object to be inspect, redacted, and upload. - * @param inspectOptions The options for {@link BushClientUtil.inspect}. - * @returns The {@link HasteResults}. - */ - public async inspectCleanRedactHaste(input: any, inspectOptions?: BushInspectOptions): Promise<HasteResults> { - input = inspect(input, inspectOptions ?? undefined); - input = this.redact(input); - return this.haste(input, true); - } - - /** - * Takes an any value, inspects it and redacts credentials. - * @param input The object to be inspect and redacted. - * @param inspectOptions The options for {@link BushClientUtil.inspect}. - * @returns The redacted and inspected object. - */ - public inspectAndRedact(input: any, inspectOptions?: BushInspectOptions): string { - input = inspect(input, inspectOptions ?? undefined); - return this.redact(input); - } - - /** - * Get the global cache. - */ - public getGlobal(): GlobalCache; - /** - * Get a key from the global cache. - * @param key The key to get in the global cache. - */ - public getGlobal<K extends keyof GlobalCache>(key: K): GlobalCache[K]; - public getGlobal(key?: keyof GlobalCache) { - return key ? this.client.cache.global[key] : this.client.cache.global; - } - - /** - * Get the shared cache. - */ - public getShared(): SharedCache; - /** - * Get a key from the shared cache. - * @param key The key to get in the shared cache. - */ - public getShared<K extends keyof SharedCache>(key: K): SharedCache[K]; - public getShared(key?: keyof SharedCache) { - return key ? this.client.cache.shared[key] : this.client.cache.shared; - } - - /** - * Add or remove an element from an array stored in the Globals database. - * @param action Either `add` or `remove` an element. - * @param key The key of the element in the global cache to update. - * @param value The value to add/remove from the array. - */ - public async insertOrRemoveFromGlobal<K extends keyof Client['cache']['global']>( - action: 'add' | 'remove', - key: K, - value: Client['cache']['global'][K][0] - ): Promise<Global | void> { - const row = - (await Global.findByPk(this.client.config.environment)) ?? - (await Global.create({ environment: this.client.config.environment })); - const oldValue: any[] = row[key]; - const newValue = addOrRemoveFromArray(action, oldValue, value); - row[key] = newValue; - this.client.cache.global[key] = newValue; - return await row.save().catch((e) => this.handleError('insertOrRemoveFromGlobal', e)); - } - - /** - * Add or remove an element from an array stored in the Shared database. - * @param action Either `add` or `remove` an element. - * @param key The key of the element in the shared cache to update. - * @param value The value to add/remove from the array. - */ - public async insertOrRemoveFromShared<K extends Exclude<keyof Client['cache']['shared'], 'badWords' | 'autoBanCode'>>( - action: 'add' | 'remove', - key: K, - value: Client['cache']['shared'][K][0] - ): Promise<Shared | void> { - const row = (await Shared.findByPk(0)) ?? (await Shared.create()); - const oldValue: any[] = row[key]; - const newValue = addOrRemoveFromArray(action, oldValue, value); - row[key] = newValue; - this.client.cache.shared[key] = newValue; - return await row.save().catch((e) => this.handleError('insertOrRemoveFromShared', e)); - } - - /** - * Updates an element in the Globals database. - * @param key The key in the global cache to update. - * @param value The value to set the key to. - */ - public async setGlobal<K extends keyof Client['cache']['global']>( - key: K, - value: Client['cache']['global'][K] - ): Promise<Global | void> { - const row = - (await Global.findByPk(this.client.config.environment)) ?? - (await Global.create({ environment: this.client.config.environment })); - row[key] = value; - this.client.cache.global[key] = value; - return await row.save().catch((e) => this.handleError('setGlobal', e)); - } - - /** - * Updates an element in the Shared database. - * @param key The key in the shared cache to update. - * @param value The value to set the key to. - */ - public async setShared<K extends Exclude<keyof Client['cache']['shared'], 'badWords' | 'autoBanCode'>>( - key: K, - value: Client['cache']['shared'][K] - ): Promise<Shared | void> { - const row = (await Shared.findByPk(0)) ?? (await Shared.create()); - row[key] = value; - this.client.cache.shared[key] = value; - return await row.save().catch((e) => this.handleError('setShared', e)); - } - - /** - * Send a message in the error logging channel and console for an error. - * @param context - * @param error - */ - public async handleError(context: string, error: Error) { - await this.client.console.error(_.camelCase(context), `An error occurred:\n${formatError(error, false)}`, false); - await this.client.console.channelError({ - embeds: await CommandErrorListener.generateErrorEmbed(this.client, { type: 'unhandledRejection', error: error, context }) - }); - } - - /** - * Fetches a user from discord. - * @param user The user to fetch - * @returns Undefined if the user is not found, otherwise the user. - */ - public async resolveNonCachedUser(user: UserResolvable | undefined | null): Promise<User | undefined> { - if (user == null) return undefined; - const resolvedUser = - user instanceof User - ? user - : user instanceof GuildMember - ? user.user - : user instanceof ThreadMember - ? user.user - : user instanceof Message - ? user.author - : undefined; - - return resolvedUser ?? (await this.client.users.fetch(user as Snowflake).catch(() => undefined)); - } - - /** - * Get the pronouns of a discord user from pronoundb.org - * @param user The user to retrieve the promises of. - * @returns The human readable pronouns of the user, or undefined if they do not have any. - */ - public async getPronounsOf(user: User | Snowflake): Promise<Pronoun | undefined> { - const _user = await this.resolveNonCachedUser(user); - if (!_user) throw new Error(`Cannot find user ${user}`); - const apiRes = (await got - .get(`https://pronoundb.org/api/v1/lookup?platform=discord&id=${_user.id}`) - .json() - .catch(() => undefined)) as { pronouns: PronounCode } | undefined; - - if (!apiRes) return undefined; - assert(apiRes.pronouns); - - return pronounMapping[apiRes.pronouns!]!; - } - - /** - * Uploads an image to imgur. - * @param image The image to upload. - * @returns The url of the imgur. - */ - public async uploadImageToImgur(image: string) { - const clientId = this.client.config.credentials.imgurClientId; - - const resp = (await got - .post('https://api.imgur.com/3/upload', { - headers: { - Authorization: `Client-ID ${clientId}`, - Accept: 'application/json' - }, - form: { - image: image, - type: 'base64' - }, - followRedirect: true - }) - .json()) as { data: { link: string } }; - - return resp.data.link; - } - - /** - * Gets the prefix based off of the message. - * @param message The message to get the prefix from. - * @returns The prefix. - */ - public prefix(message: CommandMessage | SlashMessage): string { - return message.util.isSlash - ? '/' - : this.client.config.isDevelopment - ? 'dev ' - : message.util.parsed?.prefix ?? this.client.config.prefix; - } - - public async resolveMessageLinks(content: string | null): Promise<MessageLinkParts[]> { - const res: MessageLinkParts[] = []; - - if (!content) return res; - - const regex_ = new RegExp(regex.messageLink); - let match: RegExpExecArray | null; - while (((match = regex_.exec(content)), match !== null)) { - const input = match.input; - if (!match.groups || !input) continue; - if (input.startsWith('<') && input.endsWith('>')) continue; - - const { guild_id, channel_id, message_id } = match.groups; - if (!guild_id || !channel_id || !message_id) continue; - - res.push({ guild_id, channel_id, message_id }); - } - - return res; - } - - public async resolveMessagesFromLinks(content: string): Promise<APIMessage[]> { - const res: APIMessage[] = []; - - const links = await this.resolveMessageLinks(content); - if (!links.length) return []; - - for (const { guild_id, channel_id, message_id } of links) { - const guild = this.client.guilds.cache.get(guild_id); - if (!guild) continue; - const channel = guild.channels.cache.get(channel_id); - if (!channel || (!channel.isTextBased() && !channel.isThread())) continue; - - const message = (await this.client.rest - .get(Routes.channelMessage(channel_id, message_id)) - .catch(() => null)) as APIMessage | null; - if (!message) continue; - - res.push(message); - } - - return res; - } - - /** - * Resolves a channel from the config and ensures it is a non-dm-based-text-channel. - * @param channel The channel to retrieve. - */ - public async getConfigChannel( - channel: ConfigChannelKey - ): Promise<Exclude<TextBasedChannel, DMChannel | PartialDMChannel> | null> { - const channels = this.client.config.channels; - if (!(channel in channels)) - throw new TypeError(`Invalid channel provided (${channel}), must be one of ${Object.keys(channels).join(' ')}`); - - const channelId = channels[channel]; - if (channelId === '') return null; - - const res = await this.client.channels.fetch(channelId); - - if (!res?.isTextBased() || res.isDMBased()) return null; - - return res; - } -} - -interface HastebinRes { - key: string; -} - -export interface HasteResults { - url?: string; - error?: 'content too long' | 'substr' | 'unable to post'; -} - -export interface MessageLinkParts { - guild_id: Snowflake; - channel_id: Snowflake; - message_id: Snowflake; -} diff --git a/src/lib/utils/BushConstants.ts b/src/lib/utils/BushConstants.ts deleted file mode 100644 index 090616c..0000000 --- a/src/lib/utils/BushConstants.ts +++ /dev/null @@ -1,531 +0,0 @@ -import deepLock from 'deep-lock'; -import { - ArgumentMatches as AkairoArgumentMatches, - ArgumentTypes as AkairoArgumentTypes, - BuiltInReasons, - CommandHandlerEvents as AkairoCommandHandlerEvents -} from 'discord-akairo/dist/src/util/Constants.js'; -import { Colors, GuildFeature } from 'discord.js'; - -const rawCapeUrl = 'https://raw.githubusercontent.com/NotEnoughUpdates/capes/master/'; - -/** - * Time units in milliseconds - */ -export const enum Time { - /** - * One millisecond (1 ms). - */ - Millisecond = 1, - - /** - * One second (1,000 ms). - */ - Second = Millisecond * 1000, - - /** - * One minute (60,000 ms). - */ - Minute = Second * 60, - - /** - * One hour (3,600,000 ms). - */ - Hour = Minute * 60, - - /** - * One day (86,400,000 ms). - */ - Day = Hour * 24, - - /** - * One week (604,800,000 ms). - */ - Week = Day * 7, - - /** - * One month (2,629,800,000 ms). - */ - Month = Day * 30.4375, // average of days in a month (including leap years) - - /** - * One year (31,557,600,000 ms). - */ - Year = Day * 365.25 // average with leap years -} - -export const emojis = Object.freeze({ - success: '<:success:837109864101707807>', - warn: '<:warn:848726900876247050>', - error: '<:error:837123021016924261>', - successFull: '<:success_full:850118767576088646>', - warnFull: '<:warn_full:850118767391539312>', - errorFull: '<:error_full:850118767295201350>', - mad: '<:mad:783046135392239626>', - join: '<:join:850198029809614858>', - leave: '<:leave:850198048205307919>', - loading: '<a:Loading:853419254619963392>', - offlineCircle: '<:offline:787550565382750239>', - dndCircle: '<:dnd:787550487633330176>', - idleCircle: '<:idle:787550520956551218>', - onlineCircle: '<:online:787550449435803658>', - cross: '<:cross:878319362539421777>', - check: '<:check:878320135297961995>' -} as const); - -export const emojisRaw = Object.freeze({ - success: '837109864101707807', - warn: '848726900876247050', - error: '837123021016924261', - successFull: '850118767576088646', - warnFull: '850118767391539312', - errorFull: '850118767295201350', - mad: '783046135392239626', - join: '850198029809614858', - leave: '850198048205307919', - loading: '853419254619963392', - offlineCircle: '787550565382750239', - dndCircle: '787550487633330176', - idleCircle: '787550520956551218', - onlineCircle: '787550449435803658', - cross: '878319362539421777', - check: '878320135297961995' -} as const); - -export const colors = Object.freeze({ - default: 0x1fd8f1, - error: 0xef4947, - warn: 0xfeba12, - success: 0x3bb681, - info: 0x3b78ff, - red: 0xff0000, - blue: 0x0055ff, - aqua: 0x00bbff, - purple: 0x8400ff, - blurple: 0x5440cd, - newBlurple: 0x5865f2, - pink: 0xff00e6, - green: 0x00ff1e, - darkGreen: 0x008f11, - gold: 0xb59400, - yellow: 0xffff00, - white: 0xffffff, - gray: 0xa6a6a6, - lightGray: 0xcfcfcf, - darkGray: 0x7a7a7a, - black: 0x000000, - orange: 0xe86100, - ...Colors -} as const); - -// Somewhat stolen from @Mzato0001 -export const timeUnits = deepLock({ - milliseconds: { - match: / (?:(?<milliseconds>-?(?:\d+)?\.?\d+) *(?:milliseconds?|msecs?|ms))/im, - value: Time.Millisecond - }, - seconds: { - match: / (?:(?<seconds>-?(?:\d+)?\.?\d+) *(?:seconds?|secs?|s))/im, - value: Time.Second - }, - minutes: { - match: / (?:(?<minutes>-?(?:\d+)?\.?\d+) *(?:minutes?|mins?|m))/im, - value: Time.Minute - }, - hours: { - match: / (?:(?<hours>-?(?:\d+)?\.?\d+) *(?:hours?|hrs?|h))/im, - value: Time.Hour - }, - days: { - match: / (?:(?<days>-?(?:\d+)?\.?\d+) *(?:days?|d))/im, - value: Time.Day - }, - weeks: { - match: / (?:(?<weeks>-?(?:\d+)?\.?\d+) *(?:weeks?|w))/im, - value: Time.Week - }, - months: { - match: / (?:(?<months>-?(?:\d+)?\.?\d+) *(?:months?|mon|mo))/im, - value: Time.Month - }, - years: { - match: / (?:(?<years>-?(?:\d+)?\.?\d+) *(?:years?|y))/im, - value: Time.Year - } -} as const); - -export const regex = deepLock({ - snowflake: /^\d{15,21}$/im, - - discordEmoji: /<a?:(?<name>[a-zA-Z0-9_]+):(?<id>\d{15,21})>/im, - - /* - * Taken with permission from Geek: - * https://github.com/FireDiscordBot/bot/blob/5d1990e5f8b52fcc72261d786aa3c7c7c65ab5e8/lib/util/constants.ts#L276 - */ - /** **This has the global flag, make sure to handle it correctly.** */ - messageLink: - /<?https:\/\/(?:ptb\.|canary\.|staging\.)?discord(?:app)?\.com?\/channels\/(?<guild_id>\d{15,21})\/(?<channel_id>\d{15,21})\/(?<message_id>\d{15,21})>?/gim -} as const); - -/** - * Maps the response from pronoundb.org to a readable format - */ -export const pronounMapping = Object.freeze({ - unspecified: 'Unspecified', - hh: 'He/Him', - hi: 'He/It', - hs: 'He/She', - ht: 'He/They', - ih: 'It/Him', - ii: 'It/Its', - is: 'It/She', - it: 'It/They', - shh: 'She/He', - sh: 'She/Her', - si: 'She/It', - st: 'She/They', - th: 'They/He', - ti: 'They/It', - ts: 'They/She', - tt: 'They/Them', - any: 'Any pronouns', - other: 'Other pronouns', - ask: 'Ask me my pronouns', - avoid: 'Avoid pronouns, use my name' -} as const); - -/** - * A bunch of mappings - */ -export const mappings = deepLock({ - guilds: { - "Moulberry's Bush": '516977525906341928', - "Moulberry's Tree": '767448775450820639', - 'MB Staff': '784597260465995796', - "IRONM00N's Space Ship": '717176538717749358' - }, - - channels: { - 'neu-support': '714332750156660756', - 'giveaways': '767782084981817344' - }, - - users: { - IRONM00N: '322862723090219008', - Moulberry: '211288288055525376', - nopo: '384620942577369088', - Bestower: '496409778822709251' - }, - - permissions: { - CreateInstantInvite: { name: 'Create Invite', important: false }, - KickMembers: { name: 'Kick Members', important: true }, - BanMembers: { name: 'Ban Members', important: true }, - Administrator: { name: 'Administrator', important: true }, - ManageChannels: { name: 'Manage Channels', important: true }, - ManageGuild: { name: 'Manage Server', important: true }, - AddReactions: { name: 'Add Reactions', important: false }, - ViewAuditLog: { name: 'View Audit Log', important: true }, - PrioritySpeaker: { name: 'Priority Speaker', important: true }, - Stream: { name: 'Video', important: false }, - ViewChannel: { name: 'View Channel', important: false }, - SendMessages: { name: 'Send Messages', important: false }, - SendTTSMessages: { name: 'Send Text-to-Speech Messages', important: true }, - ManageMessages: { name: 'Manage Messages', important: true }, - EmbedLinks: { name: 'Embed Links', important: false }, - AttachFiles: { name: 'Attach Files', important: false }, - ReadMessageHistory: { name: 'Read Message History', important: false }, - MentionEveryone: { name: 'Mention @\u200Beveryone, @\u200Bhere, and All Roles', important: true }, // name has a zero-width space to prevent accidents - UseExternalEmojis: { name: 'Use External Emoji', important: false }, - ViewGuildInsights: { name: 'View Server Insights', important: true }, - Connect: { name: 'Connect', important: false }, - Speak: { name: 'Speak', important: false }, - MuteMembers: { name: 'Mute Members', important: true }, - DeafenMembers: { name: 'Deafen Members', important: true }, - MoveMembers: { name: 'Move Members', important: true }, - UseVAD: { name: 'Use Voice Activity', important: false }, - ChangeNickname: { name: 'Change Nickname', important: false }, - ManageNicknames: { name: 'Change Nicknames', important: true }, - ManageRoles: { name: 'Manage Roles', important: true }, - ManageWebhooks: { name: 'Manage Webhooks', important: true }, - ManageEmojisAndStickers: { name: 'Manage Emojis and Stickers', important: true }, - UseApplicationCommands: { name: 'Use Slash Commands', important: false }, - RequestToSpeak: { name: 'Request to Speak', important: false }, - ManageEvents: { name: 'Manage Events', important: true }, - ManageThreads: { name: 'Manage Threads', important: true }, - CreatePublicThreads: { name: 'Create Public Threads', important: false }, - CreatePrivateThreads: { name: 'Create Private Threads', important: false }, - UseExternalStickers: { name: 'Use External Stickers', important: false }, - SendMessagesInThreads: { name: 'Send Messages In Threads', important: false }, - StartEmbeddedActivities: { name: 'Start Activities', important: false }, - ModerateMembers: { name: 'Timeout Members', important: true }, - UseEmbeddedActivities: { name: 'Use Activities', important: false } - }, - - // prettier-ignore - features: { - [GuildFeature.Verified]: { name: 'Verified', important: true, emoji: '<:verified:850795049817473066>', weight: 0 }, - [GuildFeature.Partnered]: { name: 'Partnered', important: true, emoji: '<:partneredServer:850794851955507240>', weight: 1 }, - [GuildFeature.MoreStickers]: { name: 'More Stickers', important: true, emoji: null, weight: 2 }, - MORE_EMOJIS: { name: 'More Emoji', important: true, emoji: '<:moreEmoji:850786853497602080>', weight: 3 }, - [GuildFeature.Featurable]: { name: 'Featurable', important: true, emoji: '<:featurable:850786776372084756>', weight: 4 }, - [GuildFeature.RelayEnabled]: { name: 'Relay Enabled', important: true, emoji: '<:relayEnabled:850790531441229834>', weight: 5 }, - [GuildFeature.Discoverable]: { name: 'Discoverable', important: true, emoji: '<:discoverable:850786735360966656>', weight: 6 }, - ENABLED_DISCOVERABLE_BEFORE: { name: 'Enabled Discovery Before', important: false, emoji: '<:enabledDiscoverableBefore:850786754670624828>', weight: 7 }, - [GuildFeature.MonetizationEnabled]: { name: 'Monetization Enabled', important: true, emoji: null, weight: 8 }, - [GuildFeature.TicketedEventsEnabled]: { name: 'Ticketed Events Enabled', important: true, emoji: null, weight: 9 }, - [GuildFeature.PreviewEnabled]: { name: 'Preview Enabled', important: true, emoji: '<:previewEnabled:850790508266913823>', weight: 10 }, - COMMERCE: { name: 'Store Channels', important: true, emoji: '<:storeChannels:850786692432396338>', weight: 11 }, - [GuildFeature.VanityURL]: { name: 'Vanity URL', important: false, emoji: '<:vanityURL:850790553079644160>', weight: 12 }, - [GuildFeature.VIPRegions]: { name: 'VIP Regions', important: false, emoji: '<:VIPRegions:850794697496854538>', weight: 13 }, - [GuildFeature.AnimatedIcon]: { name: 'Animated Icon', important: false, emoji: '<:animatedIcon:850774498071412746>', weight: 14 }, - [GuildFeature.Banner]: { name: 'Banner', important: false, emoji: '<:banner:850786673150787614>', weight: 15 }, - [GuildFeature.InviteSplash]: { name: 'Invite Splash', important: false, emoji: '<:inviteSplash:850786798246559754>', weight: 16 }, - [GuildFeature.PrivateThreads]: { name: 'Private Threads', important: false, emoji: '<:privateThreads:869763711894700093>', weight: 17 }, - THREE_DAY_THREAD_ARCHIVE: { name: 'Three Day Thread Archive', important: false, emoji: '<:threeDayThreadArchive:869767841652564008>', weight: 19 }, - SEVEN_DAY_THREAD_ARCHIVE: { name: 'Seven Day Thread Archive', important: false, emoji: '<:sevenDayThreadArchive:869767896123998288>', weight: 20 }, - [GuildFeature.RoleIcons]: { name: 'Role Icons', important: false, emoji: '<:roleIcons:876993381929222175>', weight: 21 }, - [GuildFeature.News]: { name: 'Announcement Channels', important: false, emoji: '<:announcementChannels:850790491796013067>', weight: 22 }, - [GuildFeature.MemberVerificationGateEnabled]: { name: 'Membership Verification Gate', important: false, emoji: '<:memberVerificationGateEnabled:850786829984858212>', weight: 23 }, - [GuildFeature.WelcomeScreenEnabled]: { name: 'Welcome Screen Enabled', important: false, emoji: '<:welcomeScreenEnabled:850790575875817504>', weight: 24 }, - [GuildFeature.Community]: { name: 'Community', important: false, emoji: '<:community:850786714271875094>', weight: 25 }, - THREADS_ENABLED: {name: 'Threads Enabled', important: false, emoji: '<:threadsEnabled:869756035345317919>', weight: 26 }, - THREADS_ENABLED_TESTING: {name: 'Threads Enabled Testing', important: false, emoji: null, weight: 27 }, - [GuildFeature.AnimatedBanner]: { name: 'Animated Banner', important: false, emoji: null, weight: 28 }, - [GuildFeature.HasDirectoryEntry]: { name: 'Has Directory Entry', important: true, emoji: null, weight: 29 }, - [GuildFeature.Hub]: { name: 'Hub', important: true, emoji: null, weight: 30 }, - [GuildFeature.LinkedToHub]: { name: 'Linked To Hub', important: true, emoji: null, weight: 31 }, - }, - - regions: { - 'automatic': ':united_nations: Automatic', - 'brazil': ':flag_br: Brazil', - 'europe': ':flag_eu: Europe', - 'hongkong': ':flag_hk: Hongkong', - 'india': ':flag_in: India', - 'japan': ':flag_jp: Japan', - 'russia': ':flag_ru: Russia', - 'singapore': ':flag_sg: Singapore', - 'southafrica': ':flag_za: South Africa', - 'sydney': ':flag_au: Sydney', - 'us-central': ':flag_us: US Central', - 'us-east': ':flag_us: US East', - 'us-south': ':flag_us: US South', - 'us-west': ':flag_us: US West' - }, - - otherEmojis: { - ServerBooster1: '<:serverBooster1:848740052091142145>', - ServerBooster2: '<:serverBooster2:848740090506510388>', - ServerBooster3: '<:serverBooster3:848740124992077835>', - ServerBooster6: '<:serverBooster6:848740155245461514>', - ServerBooster9: '<:serverBooster9:848740188846030889>', - ServerBooster12: '<:serverBooster12:848740304365551668>', - ServerBooster15: '<:serverBooster15:848740354890137680>', - ServerBooster18: '<:serverBooster18:848740402886606868>', - ServerBooster24: '<:serverBooster24:848740444628320256>', - Nitro: '<:nitro:848740498054971432>', - Booster: '<:booster:848747775020892200>', - Owner: '<:owner:848746439311753286>', - Admin: '<:admin:848963914628333598>', - Superuser: '<:superUser:848947986326224926>', - Developer: '<:developer:848954538111139871>', - Bot: '<:bot:1006929813203853427>', - BushVerified: '<:verfied:853360152090771497>', - BoostTier1: '<:boostitle:853363736679940127>', - BoostTier2: '<:boostitle:853363752728789075>', - BoostTier3: '<:boostitle:853363769132056627>', - ChannelText: '<:text:853375537791893524>', - ChannelNews: '<:announcements:853375553531674644>', - ChannelVoice: '<:voice:853375566735212584>', - ChannelStage: '<:stage:853375583521210468>', - // ChannelStore: '<:store:853375601175691266>', - ChannelCategory: '<:category:853375615260819476>', - ChannelThread: '<:thread:865033845753249813>' - }, - - userFlags: { - Staff: '<:discordEmployee:848742947826434079>', - Partner: '<:partneredServerOwner:848743051593777152>', - Hypesquad: '<:hypeSquadEvents:848743108283072553>', - BugHunterLevel1: '<:bugHunter:848743239850393640>', - HypeSquadOnlineHouse1: '<:hypeSquadBravery:848742910563844127>', - HypeSquadOnlineHouse2: '<:hypeSquadBrilliance:848742840649646101>', - HypeSquadOnlineHouse3: '<:hypeSquadBalance:848742877537370133>', - PremiumEarlySupporter: '<:earlySupporter:848741030102171648>', - TeamPseudoUser: 'TeamPseudoUser', - BugHunterLevel2: '<:bugHunterGold:848743283080822794>', - VerifiedBot: '<:verifiedbot_rebrand1:938928232667947028><:verifiedbot_rebrand2:938928355707879475>', - VerifiedDeveloper: '<:earlyVerifiedBotDeveloper:848741079875846174>', - CertifiedModerator: '<:discordCertifiedModerator:877224285901582366>', - BotHTTPInteractions: 'BotHTTPInteractions', - Spammer: 'Spammer', - Quarantined: 'Quarantined' - }, - - 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', purchasable: false /* moulberry no longer offers */ }, - { name: 'patreon2', purchasable: false /* moulberry no longer offers */ }, - { name: 'fade', custom: `${rawCapeUrl}fade.gif`, purchasable: true }, - { name: 'lava', custom: `${rawCapeUrl}lava.gif`, purchasable: true }, - { name: 'mcworld', custom: `${rawCapeUrl}mcworld_compressed.gif`, purchasable: true }, - { name: 'negative', custom: `${rawCapeUrl}negative_compressed.gif`, purchasable: true }, - { name: 'space', custom: `${rawCapeUrl}space_compressed.gif`, purchasable: true }, - { name: 'void', custom: `${rawCapeUrl}void.gif`, purchasable: true }, - { name: 'tunnel', custom: `${rawCapeUrl}tunnel.gif`, purchasable: true }, - /* Staff capes */ - { name: 'contrib' }, - { name: 'mbstaff' }, - { name: 'ironmoon' }, - { name: 'gravy' }, - { name: 'nullzee' }, - /* partner capes */ - { name: 'thebakery' }, - { name: 'dsm' }, - { name: 'packshq' }, - { name: 'furf' }, - { name: 'skytils' }, - { name: 'sbp' }, - { name: 'subreddit_light' }, - { name: 'subreddit_dark' }, - { name: 'skyclient' }, - { name: 'sharex' }, - { name: 'sharex_white' }, - /* streamer capes */ - { name: 'alexxoffi' }, - { name: 'jakethybro' }, - { name: 'krusty' }, - { name: 'krusty_day' }, - { name: 'krusty_night' }, - { name: 'krusty_sunset' }, - { name: 'soldier' }, - { name: 'zera' }, - { name: 'secondpfirsisch' }, - { name: 'stormy_lh' } - ].map((value, index) => ({ ...value, index })), - - 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' } - ], - - 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'] - } -} as const); - -export const ArgumentMatches = Object.freeze({ - ...AkairoArgumentMatches -} as const); - -export const ArgumentTypes = Object.freeze({ - ...AkairoArgumentTypes, - DURATION: 'duration', - CONTENT_WITH_DURATION: 'contentWithDuration', - PERMISSION: 'permission', - SNOWFLAKE: 'snowflake', - DISCORD_EMOJI: 'discordEmoji', - ROLE_WITH_DURATION: 'roleWithDuration', - ABBREVIATED_NUMBER: 'abbreviatedNumber', - GLOBAL_USER: 'globalUser' -} as const); - -export const BlockedReasons = Object.freeze({ - ...BuiltInReasons, - DISABLED_GUILD: 'disabledGuild', - DISABLED_GLOBAL: 'disabledGlobal', - ROLE_BLACKLIST: 'roleBlacklist', - USER_GUILD_BLACKLIST: 'userGuildBlacklist', - USER_GLOBAL_BLACKLIST: 'userGlobalBlacklist', - RESTRICTED_GUILD: 'restrictedGuild', - CHANNEL_GUILD_BLACKLIST: 'channelGuildBlacklist', - CHANNEL_GLOBAL_BLACKLIST: 'channelGlobalBlacklist', - RESTRICTED_CHANNEL: 'restrictedChannel' -} as const); - -export const CommandHandlerEvents = Object.freeze({ - ...AkairoCommandHandlerEvents -} as const); - -export const moulberryBushRoleMap = deepLock([ - { 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' } -] as const); - -export type PronounCode = keyof typeof pronounMapping; -export type Pronoun = typeof pronounMapping[PronounCode]; diff --git a/src/lib/utils/BushLogger.ts b/src/lib/utils/BushLogger.ts deleted file mode 100644 index 4acda69..0000000 --- a/src/lib/utils/BushLogger.ts +++ /dev/null @@ -1,315 +0,0 @@ -import chalk from 'chalk'; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import { bold, Client, EmbedBuilder, escapeMarkdown, PartialTextBasedChannelFields, type Message } from 'discord.js'; -import { stripVTControlCharacters as stripColor } from 'node:util'; -import repl, { REPLServer, REPL_MODE_STRICT } from 'repl'; -import { WriteStream } from 'tty'; -import { type SendMessageType } from '../extensions/discord-akairo/BushClient.js'; -import { colors } from './BushConstants.js'; -import { inspect } from './BushUtils.js'; - -let REPL: REPLServer; -let replGone = false; - -export function init() { - const kFormatForStdout = Object.getOwnPropertySymbols(console).find((sym) => sym.toString() === 'Symbol(kFormatForStdout)')!; - const kFormatForStderr = Object.getOwnPropertySymbols(console).find((sym) => sym.toString() === 'Symbol(kFormatForStderr)')!; - - REPL = repl.start({ - useColors: true, - terminal: true, - useGlobal: true, - replMode: REPL_MODE_STRICT, - breakEvalOnSigint: true, - ignoreUndefined: true - }); - - const apply = (stream: WriteStream, symbol: symbol): ProxyHandler<typeof console['log']>['apply'] => - function apply(target, thisArg, args) { - if (stream.isTTY) { - stream.moveCursor(0, -1); - stream.write('\n'); - stream.clearLine(0); - } - - const ret = target(...args); - - if (stream.isTTY) { - const formatted = (console as any)[symbol](args) as string; - - stream.moveCursor(0, formatted.split('\n').length); - if (!replGone) { - REPL.displayPrompt(true); - } - } - - return ret; - }; - - global.console.log = new Proxy(console.log, { - apply: apply(process.stdout, kFormatForStdout) - }); - - global.console.warn = new Proxy(console.warn, { - apply: apply(process.stderr, kFormatForStderr) - }); - - REPL.on('exit', () => { - replGone = true; - process.exit(0); - }); -} - -/** - * Parses the content surrounding by `<<>>` and emphasizes it with the given color or by making it bold. - * @param content The content to parse. - * @param color The color to emphasize the content with. - * @param discordFormat Whether or not to format the content for discord. - * @returns The formatted content. - */ -function parseFormatting( - content: any, - color: 'blueBright' | 'blackBright' | 'redBright' | 'yellowBright' | 'greenBright' | '', - discordFormat = false -): string | typeof content { - if (typeof content !== 'string') return content; - return content - .split(/<<|>>/) - .map((value, index) => { - if (discordFormat) { - return index % 2 === 0 ? escapeMarkdown(value) : bold(escapeMarkdown(value)); - } else { - return index % 2 === 0 || !color ? value : chalk[color](value); - } - }) - .join(''); -} - -/** - * Inspects the content and returns a string. - * @param content The content to inspect. - * @param depth The depth the content will inspected. Defaults to `2`. - * @param colors Whether or not to use colors in the output. Defaults to `true`. - * @returns The inspected content. - */ -function inspectContent(content: any, depth = 2, colors = true): string { - if (typeof content !== 'string') { - return inspect(content, { depth, colors }); - } - return content; -} - -/** - * Generates a formatted timestamp for logging. - * @returns The formatted timestamp. - */ -function getTimeStamp(): string { - const now = new Date(); - const minute = pad(now.getMinutes()); - const hour = pad(now.getHours()); - const date = `${pad(now.getMonth() + 1)}/${pad(now.getDate())}`; - return `${date} ${hour}:${minute}`; -} - -/** - * Pad a two-digit number. - */ -function pad(num: number) { - return num.toString().padStart(2, '0'); -} - -/** - * Custom logging utility for the bot. - */ -export class BushLogger { - /** - * @param client The client. - */ - public constructor(public client: Client) {} - - /** - * Logs information. Highlight information by surrounding it in `<<>>`. - * @param header The header displayed before the content, displayed in cyan. - * @param content The content to log, highlights displayed in bright blue. - * @param sendChannel Should this also be logged to discord? Defaults to false. - * @param depth The depth the content will inspected. Defaults to 0. - */ - public get log() { - return this.info; - } - - /** - * Sends a message to the log channel. - * @param message The parameter to pass to {@link PartialTextBasedChannelFields.send}. - * @returns The message sent. - */ - public async channelLog(message: SendMessageType): Promise<Message | null> { - const channel = await this.client.utils.getConfigChannel('log'); - if (channel === null) return null; - return await channel.send(message).catch(() => null); - } - - /** - * Sends a message to the error channel. - * @param message The parameter to pass to {@link PartialTextBasedChannelFields.send}. - * @returns The message sent. - */ - public async channelError(message: SendMessageType): Promise<Message | null> { - const channel = await this.client.utils.getConfigChannel('error'); - if (!channel) { - void this.error( - 'BushLogger', - `Could not find error channel, was originally going to send: \n${inspect(message, { - colors: true - })}\n${new Error().stack?.substring(8)}`, - false - ); - return null; - } - return await channel.send(message); - } - - /** - * Logs debug information. Only works in dev is enabled in the config. - * @param content The content to log. - * @param depth The depth the content will inspected. Defaults to `0`. - */ - public debug(content: any, depth = 0): void { - if (!this.client.config.isDevelopment) return; - const newContent = inspectContent(content, depth, true); - console.log(`${chalk.bgMagenta(getTimeStamp())} ${chalk.magenta('[Debug]')} ${newContent}`); - } - - /** - * Logs raw debug information. Only works in dev is enabled in the config. - * @param content The content to log. - */ - public debugRaw(...content: any): void { - if (!this.client.config.isDevelopment) return; - console.log(`${chalk.bgMagenta(getTimeStamp())} ${chalk.magenta('[Debug]')}`, ...content); - } - - /** - * Logs verbose information. Highlight information by surrounding it in `<<>>`. - * @param header The header printed before the content, displayed in grey. - * @param content The content to log, highlights displayed in bright black. - * @param sendChannel Should this also be logged to discord? Defaults to `false`. - * @param depth The depth the content will inspected. Defaults to `0`. - */ - public async verbose(header: string, content: any, sendChannel = false, depth = 0): Promise<void> { - if (!this.client.config.logging.verbose) return; - const newContent = inspectContent(content, depth, true); - console.log(`${chalk.bgGrey(getTimeStamp())} ${chalk.grey(`[${header}]`)} ${parseFormatting(newContent, 'blackBright')}`); - if (!sendChannel) return; - const embed = new EmbedBuilder() - .setDescription(`**[${header}]** ${parseFormatting(stripColor(newContent), '', true)}`) - .setColor(colors.gray) - .setTimestamp(); - await this.channelLog({ embeds: [embed] }); - } - - /** - * Logs very verbose information. Highlight information by surrounding it in `<<>>`. - * @param header The header printed before the content, displayed in purple. - * @param content The content to log, highlights displayed in bright black. - * @param depth The depth the content will inspected. Defaults to `0`. - */ - public async superVerbose(header: string, content: any, depth = 0): Promise<void> { - if (!this.client.config.logging.verbose) return; - const newContent = inspectContent(content, depth, true); - console.log( - `${chalk.bgHex('#949494')(getTimeStamp())} ${chalk.hex('#949494')(`[${header}]`)} ${chalk.hex('#b3b3b3')(newContent)}` - ); - } - - /** - * Logs raw very verbose information. - * @param header The header printed before the content, displayed in purple. - * @param content The content to log. - */ - public async superVerboseRaw(header: string, ...content: any[]): Promise<void> { - if (!this.client.config.logging.verbose) return; - console.log(`${chalk.bgHex('#a3a3a3')(getTimeStamp())} ${chalk.hex('#a3a3a3')(`[${header}]`)}`, ...content); - } - - /** - * Logs information. Highlight information by surrounding it in `<<>>`. - * @param header The header displayed before the content, displayed in cyan. - * @param content The content to log, highlights displayed in bright blue. - * @param sendChannel Should this also be logged to discord? Defaults to `false`. - * @param depth The depth the content will inspected. Defaults to `0`. - */ - public async info(header: string, content: any, sendChannel = true, depth = 0): Promise<void> { - if (!this.client.config.logging.info) return; - const newContent = inspectContent(content, depth, true); - console.log(`${chalk.bgCyan(getTimeStamp())} ${chalk.cyan(`[${header}]`)} ${parseFormatting(newContent, 'blueBright')}`); - if (!sendChannel) return; - const embed = new EmbedBuilder() - .setDescription(`**[${header}]** ${parseFormatting(stripColor(newContent), '', true)}`) - .setColor(colors.info) - .setTimestamp(); - await this.channelLog({ embeds: [embed] }); - } - - /** - * Logs warnings. Highlight information by surrounding it in `<<>>`. - * @param header The header displayed before the content, displayed in yellow. - * @param content The content to log, highlights displayed in bright yellow. - * @param sendChannel Should this also be logged to discord? Defaults to `false`. - * @param depth The depth the content will inspected. Defaults to `0`. - */ - public async warn(header: string, content: any, sendChannel = true, depth = 0): Promise<void> { - const newContent = inspectContent(content, depth, true); - console.warn( - `${chalk.bgYellow(getTimeStamp())} ${chalk.yellow(`[${header}]`)} ${parseFormatting(newContent, 'yellowBright')}` - ); - - if (!sendChannel) return; - const embed = new EmbedBuilder() - .setDescription(`**[${header}]** ${parseFormatting(stripColor(newContent), '', true)}`) - .setColor(colors.warn) - .setTimestamp(); - await this.channelError({ embeds: [embed] }); - } - - /** - * Logs errors. Highlight information by surrounding it in `<<>>`. - * @param header The header displayed before the content, displayed in bright red. - * @param content The content to log, highlights displayed in bright red. - * @param sendChannel Should this also be logged to discord? Defaults to `false`. - * @param depth The depth the content will inspected. Defaults to `0`. - */ - public async error(header: string, content: any, sendChannel = true, depth = 0): Promise<void> { - const newContent = inspectContent(content, depth, true); - console.warn( - `${chalk.bgRedBright(getTimeStamp())} ${chalk.redBright(`[${header}]`)} ${parseFormatting(newContent, 'redBright')}` - ); - if (!sendChannel) return; - const embed = new EmbedBuilder() - .setDescription(`**[${header}]** ${parseFormatting(stripColor(newContent), '', true)}`) - .setColor(colors.error) - .setTimestamp(); - await this.channelError({ embeds: [embed] }); - return; - } - - /** - * Logs successes. Highlight information by surrounding it in `<<>>`. - * @param header The header displayed before the content, displayed in green. - * @param content The content to log, highlights displayed in bright green. - * @param sendChannel Should this also be logged to discord? Defaults to `false`. - * @param depth The depth the content will inspected. Defaults to `0`. - */ - public async success(header: string, content: any, sendChannel = true, depth = 0): Promise<void> { - const newContent = inspectContent(content, depth, true); - console.log( - `${chalk.bgGreen(getTimeStamp())} ${chalk.greenBright(`[${header}]`)} ${parseFormatting(newContent, 'greenBright')}` - ); - if (!sendChannel) return; - const embed = new EmbedBuilder() - .setDescription(`**[${header}]** ${parseFormatting(stripColor(newContent), '', true)}`) - .setColor(colors.success) - .setTimestamp(); - await this.channelLog({ embeds: [embed] }).catch(() => {}); - } -} diff --git a/src/lib/utils/BushUtils.ts b/src/lib/utils/BushUtils.ts deleted file mode 100644 index 75aded3..0000000 --- a/src/lib/utils/BushUtils.ts +++ /dev/null @@ -1,612 +0,0 @@ -import { - Arg, - BushClient, - CommandMessage, - SlashEditMessageType, - SlashSendMessageType, - timeUnits, - type BaseBushArgumentType, - type BushInspectOptions, - type SlashMessage -} from '#lib'; -import { humanizeDuration as humanizeDurationMod } from '@notenoughupdates/humanize-duration'; -import assert from 'assert/strict'; -import cp from 'child_process'; -import deepLock from 'deep-lock'; -import { Util as AkairoUtil } from 'discord-akairo'; -import { - Constants as DiscordConstants, - EmbedBuilder, - Message, - OAuth2Scopes, - PermissionFlagsBits, - PermissionsBitField, - type APIEmbed, - type APIMessage, - type CommandInteraction, - type InteractionReplyOptions, - type PermissionsString -} from 'discord.js'; -import got from 'got'; -import { DeepWritable } from 'ts-essentials'; -import { inspect as inspectUtil, promisify } from 'util'; -import * as Format from '../common/util/Format.js'; - -export type StripPrivate<T> = { [K in keyof T]: T[K] extends Record<string, any> ? StripPrivate<T[K]> : T[K] }; -export type ValueOf<T> = T[keyof T]; - -/** - * Capitalizes the first letter of the given text - * @param text The text to capitalize - * @returns The capitalized text - */ -export function capitalize(text: string): string { - return text.charAt(0).toUpperCase() + text.slice(1); -} - -export const exec = promisify(cp.exec); - -/** - * Runs a shell command and gives the output - * @param command The shell command to run - * @returns The stdout and stderr of the shell command - */ -export async function shell(command: string): Promise<{ stdout: string; stderr: string }> { - return await exec(command); -} - -/** - * Appends the correct ordinal to the given number - * @param n The number to append an ordinal to - * @returns The number with the ordinal - */ -export function 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 - */ -export function chunk<T>(arr: T[], perChunk: number): T[][] { - return arr.reduce((all, one, i) => { - const ch: number = Math.floor(i / perChunk); - (all as any[])[ch] = [].concat(all[ch] || [], one as any); - return all; - }, []); -} - -/** - * Fetches a user's uuid from the mojang api. - * @param username The username to get the uuid of. - * @returns The the uuid of the user. - */ -export async function mcUUID(username: string, dashed = false): Promise<string> { - const apiRes = (await got.get(`https://api.ashcon.app/mojang/v2/user/${username}`).json()) as UuidRes; - return dashed ? apiRes.uuid : apiRes.uuid.replace(/-/g, ''); -} - -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; -} - -/** - * Generate defaults for {@link inspect}. - * @param options The options to create defaults with. - * @returns The default options combined with the specified options. - */ -function getDefaultInspectOptions(options?: BushInspectOptions): BushInspectOptions { - return { - showHidden: options?.showHidden ?? false, - depth: options?.depth ?? 2, - colors: options?.colors ?? false, - customInspect: options?.customInspect ?? true, - showProxy: options?.showProxy ?? false, - maxArrayLength: options?.maxArrayLength ?? Infinity, - maxStringLength: options?.maxStringLength ?? Infinity, - breakLength: options?.breakLength ?? 80, - compact: options?.compact ?? 3, - sorted: options?.sorted ?? false, - getters: options?.getters ?? true, - numericSeparator: options?.numericSeparator ?? true - }; -} - -/** - * Uses {@link inspect} with custom defaults. - * @param object - The object you would like to inspect. - * @param options - The options you would like to use to inspect the object. - * @returns The inspected object. - */ -export function inspect(object: any, options?: BushInspectOptions): string { - const optionsWithDefaults = getDefaultInspectOptions(options); - - if (!optionsWithDefaults.inspectStrings && typeof object === 'string') return object; - - return inspectUtil(object, optionsWithDefaults); -} - -/** - * Responds to a slash command interaction. - * @param interaction The interaction to respond to. - * @param responseOptions The options for the response. - * @returns The message sent. - */ -export async function slashRespond( - interaction: CommandInteraction, - responseOptions: SlashSendMessageType | SlashEditMessageType -): Promise<Message | APIMessage | undefined> { - const newResponseOptions = typeof responseOptions === 'string' ? { content: responseOptions } : responseOptions; - if (interaction.replied || interaction.deferred) { - delete (newResponseOptions as InteractionReplyOptions).ephemeral; // Cannot change a preexisting message to be ephemeral - return (await interaction.editReply(newResponseOptions)) as Message | APIMessage; - } else { - await interaction.reply(newResponseOptions); - return await interaction.fetchReply().catch(() => undefined); - } -} - -/** - * Takes an array and combines the elements using the supplied conjunction. - * @param array The array to combine. - * @param conjunction The conjunction to use. - * @param ifEmpty What to return if the array is empty. - * @returns The combined elements or `ifEmpty`. - * - * @example - * const permissions = oxford(['Administrator', 'SendMessages', 'ManageMessages'], 'and', 'none'); - * console.log(permissions); // Administrator, SendMessages and ManageMessages - */ -export function oxford(array: string[], conjunction: string, ifEmpty?: string): string | undefined { - 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(', '); -} - -/** - * Add or remove an item from an array. All duplicates will be removed. - * @param action Either `add` or `remove` an element. - * @param array The array to add/remove an element from. - * @param value The element to add/remove from the array. - */ -export function addOrRemoveFromArray<T>(action: 'add' | 'remove', array: T[], value: T): T[] { - const set = new Set(array); - action === 'add' ? set.add(value) : set.delete(value); - return [...set]; -} - -/** - * Remove an item from an array. All duplicates will be removed. - * @param array The array to remove an element from. - * @param value The element to remove from the array. - */ -export function removeFromArray<T>(array: T[], value: T): T[] { - return addOrRemoveFromArray('remove', array, value); -} - -/** - * Add an item from an array. All duplicates will be removed. - * @param array The array to add an element to. - * @param value The element to add to the array. - */ -export function addToArray<T>(array: T[], value: T): T[] { - return addOrRemoveFromArray('add', array, value); -} - -/** - * Surrounds a string to the begging an end of each element in an array. - * @param array The array you want to surround. - * @param surroundChar1 The character placed in the beginning of the element. - * @param surroundChar2 The character placed in the end of the element. Defaults to `surroundChar1`. - */ -export function surroundArray(array: string[], surroundChar1: string, surroundChar2?: string): string[] { - return array.map((a) => `${surroundChar1}${a}${surroundChar2 ?? surroundChar1}`); -} - -/** - * Gets the duration from a specified string. - * @param content The string to look for a duration in. - * @param remove Whether or not to remove the duration from the original string. - * @returns The {@link ParsedDuration}. - */ -export function parseDuration(content: string, remove = true): ParsedDuration { - if (!content) return { duration: 0, content: null }; - - // eslint-disable-next-line prefer-const - let duration: number | null = null; - // Try to reduce false positives by requiring a space before the duration, this makes sure it still matches if it is - // in the beginning of the argument - let contentWithoutTime = ` ${content}`; - - for (const unit in timeUnits) { - const regex = timeUnits[unit as keyof typeof timeUnits].match; - const match = regex.exec(contentWithoutTime); - const value = Number(match?.groups?.[unit]); - if (!isNaN(value)) duration! += value * timeUnits[unit as keyof typeof timeUnits].value; - - if (remove) contentWithoutTime = contentWithoutTime.replace(regex, ''); - } - // remove the space added earlier - if (contentWithoutTime.startsWith(' ')) contentWithoutTime.replace(' ', ''); - return { duration, content: contentWithoutTime }; -} - -export interface ParsedDuration { - duration: number | null; - content: string | null; -} - -/** - * Converts a duration in milliseconds to a human readable form. - * @param duration The duration in milliseconds to convert. - * @param largest The maximum number of units to display for the duration. - * @param round Whether or not to round the smallest unit displayed. - * @returns A humanized string of the duration. - */ -export function humanizeDuration(duration: number, largest?: number, round = true): string { - if (largest) return humanizeDurationMod(duration, { language: 'en', maxDecimalPoints: 2, largest, round })!; - else return humanizeDurationMod(duration, { language: 'en', maxDecimalPoints: 2, round })!; -} - -/** - * Creates a formatted relative timestamp from a duration in milliseconds. - * @param duration The duration in milliseconds. - * @returns The formatted relative timestamp. - */ -export function timestampDuration(duration: number): string { - return `<t:${Math.round(new Date().getTime() / 1_000 + duration / 1_000)}:R>`; -} - -/** - * Creates a timestamp from a date. - * @param date The date to create a timestamp from. - * @param style The style of the timestamp. - * @returns The formatted timestamp. - * - * @see - * **Styles:** - * - **t**: Short Time ex. `16:20` - * - **T**: Long Time ex. `16:20:30 ` - * - **d**: Short Date ex. `20/04/2021` - * - **D**: Long Date ex. `20 April 2021` - * - **f**: Short Date/Time ex. `20 April 2021 16:20` - * - **F**: Long Date/Time ex. `Tuesday, 20 April 2021 16:20` - * - **R**: Relative Time ex. `2 months ago` - */ -export function timestamp<D extends Date | undefined | null>( - date: D, - style: TimestampStyle = 'f' -): D extends Date ? string : undefined { - if (!date) return date as unknown as D extends Date ? string : undefined; - return `<t:${Math.round(date.getTime() / 1_000)}:${style}>` as unknown as D extends Date ? string : undefined; -} - -export type TimestampStyle = 't' | 'T' | 'd' | 'D' | 'f' | 'F' | 'R'; - -/** - * Creates a human readable representation between a date and the current time. - * @param date The date to be compared with the current time. - * @param largest The maximum number of units to display for the duration. - * @param round Whether or not to round the smallest unit displayed. - * @returns A humanized string of the delta. - */ -export function dateDelta(date: Date, largest = 3, round = true): string { - return humanizeDuration(new Date().getTime() - date.getTime(), largest, round); -} - -/** - * Combines {@link timestamp} and {@link dateDelta} - * @param date The date to be compared with the current time. - * @param style The style of the timestamp. - * @returns The formatted timestamp. - * - * @see - * **Styles:** - * - **t**: Short Time ex. `16:20` - * - **T**: Long Time ex. `16:20:30 ` - * - **d**: Short Date ex. `20/04/2021` - * - **D**: Long Date ex. `20 April 2021` - * - **f**: Short Date/Time ex. `20 April 2021 16:20` - * - **F**: Long Date/Time ex. `Tuesday, 20 April 2021 16:20` - * - **R**: Relative Time ex. `2 months ago` - */ -export function timestampAndDelta(date: Date, style: TimestampStyle = 'D'): string { - return `${timestamp(date, style)} (${dateDelta(date)} ago)`; -} - -/** - * Convert a hex code to an rbg value. - * @param hex The hex code to convert. - * @returns The rbg value. - */ -export function hexToRgb(hex: string): string { - const arrBuff = new ArrayBuffer(4); - const vw = new DataView(arrBuff); - vw.setUint32(0, parseInt(hex, 16), false); - const arrByte = new Uint8Array(arrBuff); - - return `${arrByte[1]}, ${arrByte[2]}, ${arrByte[3]}`; -} - -/** - * Wait an amount in milliseconds. - * @returns A promise that resolves after the specified amount of milliseconds - */ -export const sleep = promisify(setTimeout); - -/** - * List the methods of an object. - * @param obj The object to get the methods of. - * @returns A string with each method on a new line. - */ -export function getMethods(obj: Record<string, any>): string { - // modified from https://stackoverflow.com/questions/31054910/get-functions-methods-of-a-class/31055217#31055217 - let props: string[] = []; - let obj_: Record<string, any> = new Object(obj); - - do { - const l = Object.getOwnPropertyNames(obj_) - .concat(Object.getOwnPropertySymbols(obj_).map((s) => s.toString())) - .sort() - .filter( - (p, i, arr) => - typeof Object.getOwnPropertyDescriptor(obj_, p)?.['get'] !== 'function' && // ignore getters - typeof Object.getOwnPropertyDescriptor(obj_, p)?.['set'] !== 'function' && // ignore setters - typeof obj_[p] === 'function' && // only the methods - p !== 'constructor' && // not the constructor - (i == 0 || p !== arr[i - 1]) && // not overriding in this prototype - props.indexOf(p) === -1 // not overridden in a child - ); - - const reg = /\(([\s\S]*?)\)/; - props = props.concat( - l.map( - (p) => - `${obj_[p] && obj_[p][Symbol.toStringTag] === 'AsyncFunction' ? 'async ' : ''}function ${p}(${ - reg.exec(obj_[p].toString())?.[1] - ? reg - .exec(obj_[p].toString())?.[1] - .split(', ') - .map((arg) => arg.split('=')[0].trim()) - .join(', ') - : '' - });` - ) - ); - } while ( - (obj_ = Object.getPrototypeOf(obj_)) && // walk-up the prototype chain - Object.getPrototypeOf(obj_) // not the the Object prototype methods (hasOwnProperty, etc...) - ); - - return props.join('\n'); -} - -/** - * List the symbols of an object. - * @param obj The object to get the symbols of. - * @returns An array of the symbols of the object. - */ -export function getSymbols(obj: Record<string, any>): symbol[] { - let symbols: symbol[] = []; - let obj_: Record<string, any> = new Object(obj); - - do { - const l = Object.getOwnPropertySymbols(obj_).sort(); - - symbols = [...symbols, ...l]; - } while ( - (obj_ = Object.getPrototypeOf(obj_)) && // walk-up the prototype chain - Object.getPrototypeOf(obj_) // not the the Object prototype methods (hasOwnProperty, etc...) - ); - - return symbols; -} - -/** - * Checks if a user has a certain guild permission (doesn't check channel permissions). - * @param message The message to check the user from. - * @param permissions The permissions to check for. - * @returns The missing permissions or null if none are missing. - */ -export function userGuildPermCheck( - message: CommandMessage | SlashMessage, - permissions: typeof PermissionFlagsBits[keyof typeof PermissionFlagsBits][] -): PermissionsString[] | null { - if (!message.inGuild()) return null; - const missing = message.member?.permissions.missing(permissions) ?? []; - - return missing.length ? missing : null; -} - -/** - * Check if the client has certain permissions in the guild (doesn't check channel permissions). - * @param message The message to check the client user from. - * @param permissions The permissions to check for. - * @returns The missing permissions or null if none are missing. - */ -export function clientGuildPermCheck(message: CommandMessage | SlashMessage, permissions: bigint[]): PermissionsString[] | null { - const missing = message.guild?.members.me?.permissions.missing(permissions) ?? []; - - return missing.length ? missing : null; -} - -/** - * Check if the client has permission to send messages in the channel as well as check if they have other permissions - * in the guild (or the channel if `checkChannel` is `true`). - * @param message The message to check the client user from. - * @param permissions The permissions to check for. - * @param checkChannel Whether to check the channel permissions instead of the guild permissions. - * @returns The missing permissions or null if none are missing. - */ -export function clientSendAndPermCheck( - message: CommandMessage | SlashMessage, - permissions: bigint[] = [], - checkChannel = false -): PermissionsString[] | null { - if (!message.inGuild() || !message.channel) return null; - - const missing: PermissionsString[] = []; - const sendPerm = message.channel.isThread() ? 'SendMessages' : 'SendMessagesInThreads'; - - // todo: remove once forum channels are fixed - if (message.channel.parent === null && message.channel.isThread()) return null; - - if (!message.guild.members.me!.permissionsIn(message.channel!.id).has(sendPerm)) missing.push(sendPerm); - - missing.push( - ...(checkChannel - ? message.guild!.members.me!.permissionsIn(message.channel!.id!).missing(permissions) - : clientGuildPermCheck(message, permissions) ?? []) - ); - - return missing.length ? missing : null; -} - -export { deepLock as deepFreeze }; -export { Arg as arg }; -export { Format as format }; -export { DiscordConstants as discordConstants }; -export { AkairoUtil as akairo }; - -/** - * The link to invite the bot with all permissions. - */ -export function invite(client: BushClient) { - return client.generateInvite({ - permissions: - PermissionsBitField.All - - PermissionFlagsBits.UseEmbeddedActivities - - PermissionFlagsBits.ViewGuildInsights - - PermissionFlagsBits.Stream, - scopes: [OAuth2Scopes.Bot, OAuth2Scopes.ApplicationsCommands] - }); -} - -/** - * Asset multiple statements at a time. - * @param args - */ -export function assertAll(...args: any[]): void { - for (let i = 0; i < args.length; i++) { - assert(args[i], `assertAll index ${i} failed`); - } -} - -/** - * Casts a string to a duration and reason for slash commands. - * @param arg The argument received. - * @param message The message that triggered the command. - * @returns The casted argument. - */ -export async function castDurationContent( - arg: string | ParsedDuration | null, - message: CommandMessage | SlashMessage -): Promise<ParsedDurationRes> { - const res = typeof arg === 'string' ? await Arg.cast('contentWithDuration', message, arg) : arg; - - return { duration: res?.duration ?? 0, content: res?.content ?? '' }; -} - -export interface ParsedDurationRes { - duration: number; - content: string; -} - -/** - * Casts a string to a the specified argument type. - * @param type The type of the argument to cast to. - * @param arg The argument received. - * @param message The message that triggered the command. - * @returns The casted argument. - */ -export async function cast<T extends keyof BaseBushArgumentType>( - type: T, - arg: BaseBushArgumentType[T] | string, - message: CommandMessage | SlashMessage -) { - return typeof arg === 'string' ? await Arg.cast(type, message, arg) : arg; -} - -/** - * Overflows the description of an embed into multiple embeds. - * @param embed The options to be applied to the (first) embed. - * @param lines Each line of the description as an element in an array. - */ -export function overflowEmbed(embed: Omit<APIEmbed, 'description'>, lines: string[], maxLength = 4096): EmbedBuilder[] { - const embeds: EmbedBuilder[] = []; - - const makeEmbed = () => { - embeds.push(new EmbedBuilder().setColor(embed.color ?? null)); - return embeds.at(-1)!; - }; - - for (const line of lines) { - let current = embeds.length ? embeds.at(-1)! : makeEmbed(); - let joined = current.data.description ? `${current.data.description}\n${line}` : line; - if (joined.length > maxLength) { - current = makeEmbed(); - joined = line; - } - - current.setDescription(joined); - } - - if (!embeds.length) makeEmbed(); - - if (embed.author) embeds.at(0)?.setAuthor(embed.author); - if (embed.title) embeds.at(0)?.setTitle(embed.title); - if (embed.url) embeds.at(0)?.setURL(embed.url); - if (embed.fields) embeds.at(-1)?.setFields(embed.fields); - if (embed.thumbnail) embeds.at(-1)?.setThumbnail(embed.thumbnail.url); - if (embed.footer) embeds.at(-1)?.setFooter(embed.footer); - if (embed.image) embeds.at(-1)?.setImage(embed.image.url); - if (embed.timestamp) embeds.at(-1)?.setTimestamp(new Date(embed.timestamp)); - - return embeds; -} - -/** - * Formats an error into a string. - * @param error The error to format. - * @param colors Whether to use colors in the output. - * @returns The formatted error. - */ -export function formatError(error: Error | any, colors = false): string { - if (!error) return error; - if (typeof error !== 'object') return String.prototype.toString.call(error); - if ( - getSymbols(error) - .map((s) => s.toString()) - .includes('Symbol(nodejs.util.inspect.custom)') - ) - return inspect(error, { colors }); - - return error.stack; -} - -export function deepWriteable<T>(obj: T): DeepWritable<T> { - return obj as DeepWritable<T>; -} diff --git a/src/lib/utils/CanvasProgressBar.ts b/src/lib/utils/CanvasProgressBar.ts deleted file mode 100644 index fb4f778..0000000 --- a/src/lib/utils/CanvasProgressBar.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { CanvasRenderingContext2D } from 'canvas'; - -/** - * I just copy pasted this code from stackoverflow don't yell at me if there is issues for it - * @author @TymanWasTaken - */ -export class CanvasProgressBar { - private readonly x: number; - private readonly y: number; - private readonly w: number; - private readonly h: number; - private readonly color: string; - private percentage: number; - private p?: number; - private ctx: CanvasRenderingContext2D; - - public 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 = undefined; - this.ctx = ctx; - } - - public 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(); - } - - // public 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(); - // } - - public get PPercentage(): number { - return this.percentage * 100; - } - - public set PPercentage(x: number) { - this.percentage = x / 100; - } -} |