diff options
author | IRONM00N <64110067+IRONM00N@users.noreply.github.com> | 2021-11-28 09:27:41 -0500 |
---|---|---|
committer | IRONM00N <64110067+IRONM00N@users.noreply.github.com> | 2021-11-28 09:27:41 -0500 |
commit | 453683b57b8ff013ff25e2aaa4aa1d2e047edcb7 (patch) | |
tree | 8b98d2f30dbb6a8448602446cfacf9091667cc33 /src/lib/extensions | |
parent | de4c3dcaf172804d34ae708be1ed3e75af42f4d5 (diff) | |
download | tanzanite-453683b57b8ff013ff25e2aaa4aa1d2e047edcb7.tar.gz tanzanite-453683b57b8ff013ff25e2aaa4aa1d2e047edcb7.tar.bz2 tanzanite-453683b57b8ff013ff25e2aaa4aa1d2e047edcb7.zip |
a few small changes
Diffstat (limited to 'src/lib/extensions')
-rw-r--r-- | src/lib/extensions/discord-akairo/BushClient.ts | 4 | ||||
-rw-r--r-- | src/lib/extensions/discord-akairo/BushClientUtil.ts | 283 | ||||
-rw-r--r-- | src/lib/extensions/discord-akairo/BushCommand.ts | 309 | ||||
-rw-r--r-- | src/lib/extensions/discord.js/BushGuild.ts | 48 | ||||
-rw-r--r-- | src/lib/extensions/global.d.ts | 9 |
5 files changed, 532 insertions, 121 deletions
diff --git a/src/lib/extensions/discord-akairo/BushClient.ts b/src/lib/extensions/discord-akairo/BushClient.ts index 3339a62..45ad7ca 100644 --- a/src/lib/extensions/discord-akairo/BushClient.ts +++ b/src/lib/extensions/discord-akairo/BushClient.ts @@ -41,6 +41,7 @@ import { discordEmoji } from '../../../arguments/discordEmoji.js'; import { duration } from '../../../arguments/duration.js'; import { durationSeconds } from '../../../arguments/durationSeconds.js'; import { globalUser } from '../../../arguments/globalUser.js'; +import { messageLink } from '../../../arguments/messageLink.js'; import { permission } from '../../../arguments/permission.js'; import { roleWithDuration } from '../../../arguments/roleWithDuration.js'; import { snowflake } from '../../../arguments/snowflake.js'; @@ -289,7 +290,8 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re roleWithDuration, abbreviatedNumber, durationSeconds, - globalUser + globalUser, + messageLink }); this.sentry = Sentry; diff --git a/src/lib/extensions/discord-akairo/BushClientUtil.ts b/src/lib/extensions/discord-akairo/BushClientUtil.ts index aa64562..889cd6e 100644 --- a/src/lib/extensions/discord-akairo/BushClientUtil.ts +++ b/src/lib/extensions/discord-akairo/BushClientUtil.ts @@ -18,9 +18,11 @@ import { } from '#lib'; import { humanizeDuration } from '@notenoughupdates/humanize-duration'; import { exec } from 'child_process'; +import deepLock from 'deep-lock'; import { ClientUtil, Util as AkairoUtil } from 'discord-akairo'; import { APIMessage } from 'discord-api-types'; import { + Constants as DiscordConstants, GuildMember, Message, MessageEmbed, @@ -37,7 +39,6 @@ import { } from 'discord.js'; import got from 'got'; import _ from 'lodash'; -import moment from 'moment'; import { inspect, promisify } from 'util'; import CommandErrorListener from '../../../listeners/commands/commandError.js'; import { Format } from '../../common/Format.js'; @@ -105,10 +106,7 @@ export class BushClientUtil extends ClientUtil { * @param content The text to post * @returns The url of the posted text */ - public async haste( - content: string, - substr = false - ): Promise<{ url?: string; error?: 'content too long' | 'substr' | 'unable to post' }> { + 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()})`)); @@ -119,7 +117,7 @@ export class BushClientUtil extends ClientUtil { } for (const url of this.#hasteURLs) { try { - const res: hastebinRes = await got.post(`${url}/documents`, { body: content }).json(); + const res: HastebinRes = await got.post(`${url}/documents`, { body: content }).json(); return { url: `${url}/${res.key}`, error: isSubstr ? 'substr' : undefined }; } catch { void client.console.error('haste', `Unable to upload haste to ${url}`); @@ -197,7 +195,10 @@ export class BushClientUtil extends ClientUtil { } /** - * A simple utility to create and embed with the needed style for the bot + * A simple utility to create and embed with the needed style for the bot. + * @param color The color to set the embed to. + * @param author The author to set the embed to. + * @returns The generated embed. */ public createEmbed(color?: ColorResolvable, author?: User | GuildMember): MessageEmbed { if (author instanceof GuildMember) { @@ -214,15 +215,25 @@ export class BushClientUtil extends ClientUtil { 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, ''); + /** + * 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. + */ + public async 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, ''); } /** * 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 = ''; @@ -250,15 +261,21 @@ export class BushClientUtil extends ClientUtil { } /** - * 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 + * 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. */ public inspect(object: any, options?: BushInspectOptions): string { const optionsWithDefaults = this.getDefaultInspectOptions(options); return inspect(object, optionsWithDefaults); } + /** + * Generate defaults for {@link inspect}. + * @param options The options to create defaults with. + * @returns The default options combined with the specified options. + */ private getDefaultInspectOptions(options?: BushInspectOptions): BushInspectOptions { const { showHidden = false, @@ -288,7 +305,12 @@ export class BushClientUtil extends ClientUtil { }; } - #mapCredential(old: string): string { + /** + * 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 { const mapping = { token: 'Main Token', devToken: 'Dev Token', @@ -297,12 +319,13 @@ export class BushClientUtil extends ClientUtil { wolframAlphaAppId: 'Wolfram|Alpha App ID', dbPassword: 'Database Password' }; - return mapping[old as keyof typeof mapping] || old; + return mapping[key as keyof typeof mapping] || key; } /** - * Redacts credentials from a string - * @param text - The text to redact credentials from + * 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 { ...client.config.credentials, dbPassword: client.config.db.password }) { @@ -321,8 +344,13 @@ export class BushClientUtil extends ClientUtil { } /** - * Takes an any value, inspects it, redacts credentials and puts it in a codeblock - * (and uploads to hast if the content is too long) + * 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, @@ -338,17 +366,35 @@ export class BushClientUtil extends ClientUtil { return this.codeblock(input, length, language, true); } - public async inspectCleanRedactHaste(input: any, inspectOptions?: BushInspectOptions) { + /** + * 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 = typeof input !== 'string' ? this.inspect(input, inspectOptions ?? undefined) : input; input = this.redact(input); return this.haste(input, true); } - public inspectAndRedact(input: any, inspectOptions?: BushInspectOptions) { + /** + * 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 = typeof input !== 'string' ? this.inspect(input, inspectOptions ?? undefined) : input; return this.redact(input); } + /** + * Responds to a slash command interaction. + * @param interaction The interaction to respond to. + * @param responseOptions The options for the response. + * @returns The message sent. + */ public async slashRespond( interaction: CommandInteraction, responseOptions: BushSlashSendMessageType | BushSlashEditMessageType @@ -364,7 +410,8 @@ export class BushClientUtil extends ClientUtil { } /** - * Gets a a configured channel as a TextChannel + * Gets a a configured channel as a TextChannel. + * @channel The channel to retrieve. */ public async getConfigChannel(channel: keyof typeof client['config']['channels']): Promise<TextChannel> { return (await client.channels.fetch(client.config.channels[channel])) as unknown as TextChannel; @@ -375,7 +422,7 @@ export class BushClientUtil extends ClientUtil { * @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` + * @returns The combined elements or `ifEmpty`. * * @example * const permissions = oxford(['ADMINISTRATOR', 'SEND_MESSAGES', 'MANAGE_MESSAGES'], 'and', 'none'); @@ -391,10 +438,16 @@ export class BushClientUtil extends ClientUtil { return array.join(', '); } - public async insertOrRemoveFromGlobal( + /** + * 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 typeof client['cache']['global']>( action: 'add' | 'remove', - key: keyof typeof client['cache']['global'], - value: any + key: K, + value: typeof client['cache']['global'][K][0] ): Promise<Global | void> { const row = (await Global.findByPk(client.config.environment)) ?? (await Global.create({ environment: client.config.environment })); @@ -406,7 +459,26 @@ export class BushClientUtil extends ClientUtil { } /** + * 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 typeof client['cache']['global']>( + key: K, + value: typeof client['cache']['global'][K] + ): Promise<Global | void> { + const row = + (await Global.findByPk(client.config.environment)) ?? (await Global.create({ environment: client.config.environment })); + row[key] = value; + client.cache.global[key] = value; + return await row.save().catch((e) => this.handleError('setGlobal', e)); + } + + /** * 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. */ public addOrRemoveFromArray<T>(action: 'add' | 'remove', array: T[], value: T): T[] { const set = new Set(array); @@ -416,15 +488,21 @@ export class BushClientUtil extends ClientUtil { /** * 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`. + * @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`. */ public surroundArray(array: string[], surroundChar1: string, surroundChar2?: string): string[] { return array.map((a) => `${surroundChar1}${a}${surroundChar2 ?? surroundChar1}`); } - public parseDuration(content: string, remove = true): { duration: number | null; contentWithoutTime: string | null } { + /** + * 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}. + */ + public parseDuration(content: string, remove = true): ParsedDuration { if (!content) return { duration: 0, contentWithoutTime: null }; // eslint-disable-next-line prefer-const @@ -434,10 +512,11 @@ export class BushClientUtil extends ClientUtil { let contentWithoutTime = ` ${content}`; for (const unit in BushConstants.TimeUnits) { - const regex = BushConstants.TimeUnits[unit].match; + const regex = BushConstants.TimeUnits[unit as keyof typeof BushConstants.TimeUnits].match; const match = regex.exec(contentWithoutTime); const value = Number(match?.groups?.[unit]); - if (!isNaN(value)) (duration as unknown as number) += value * BushConstants.TimeUnits[unit].value; + if (!isNaN(value)) + (duration as unknown as number) += value * BushConstants.TimeUnits[unit as keyof typeof BushConstants.TimeUnits].value; if (remove) contentWithoutTime = contentWithoutTime.replace(regex, ''); } @@ -446,16 +525,33 @@ export class BushClientUtil extends ClientUtil { return { duration, contentWithoutTime }; } + /** + * 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. + * @returns A humanized string of the duration. + */ public humanizeDuration(duration: number, largest?: number): string { if (largest) return humanizeDuration(duration, { language: 'en', maxDecimalPoints: 2, largest })!; else return humanizeDuration(duration, { language: 'en', maxDecimalPoints: 2 })!; } + /** + * Creates a formatted relative timestamp from a duration in milliseconds. + * @param duration The duration in milliseconds. + * @returns The formatted relative timestamp. + */ public timestampDuration(duration: number): string { - return `<t:${Math.round(duration / 1000)}:R>`; + 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 * - **T**: Long Time @@ -470,33 +566,24 @@ export class BushClientUtil extends ClientUtil { style: 't' | 'T' | 'd' | 'D' | 'f' | 'F' | 'R' = 'f' ): D extends Date ? string : undefined { if (!date) return date as unknown as D extends Date ? string : undefined; - return `<t:${Math.round(date.getTime() / 1000)}:${style}>` as unknown as D extends Date ? string : undefined; - } - - public dateDelta(date: Date, largest?: number) { - return this.humanizeDuration(moment(date).diff(moment()), largest ?? 3); + return `<t:${Math.round(date.getTime() / 1_000)}:${style}>` as unknown as D extends Date ? string : undefined; } - public async findUUID(player: string): Promise<string> { - try { - const raw = await got.get(`https://api.ashcon.app/mojang/v2/user/${player}`); - let profile: MojangProfile; - if (raw.statusCode == 200) { - profile = JSON.parse(raw.body); - } else { - throw new Error('invalid player'); - } - - if (raw.statusCode == 200 && profile && profile.uuid) { - return profile.uuid.replace(/-/g, ''); - } else { - throw new Error(`Could not fetch the uuid for ${player}.`); - } - } catch (e) { - throw new Error('An error has occurred.'); - } + /** + * 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. + * @returns A humanized string of the delta. + */ + public dateDelta(date: Date, largest?: number): string { + return this.humanizeDuration(new Date().getTime() - date.getTime(), largest ?? 3); } + /** + * Convert a hex code to an rbg value. + * @param hex The hex code to convert. + * @returns The rbg value. + */ public hexToRgb(hex: string): string { const arrBuff = new ArrayBuffer(4); const vw = new DataView(arrBuff); @@ -507,20 +594,34 @@ export class BushClientUtil extends ClientUtil { } /* eslint-disable @typescript-eslint/no-unused-vars */ - public async lockdownChannel(options: { channel: BushTextChannel | BushNewsChannel; moderator: BushUserResolvable }) {} + public async lockdownChannel(options: { channel: BushTextChannel | BushNewsChannel; moderator: BushUserResolvable }) { + // todo: implement lockdowns + } /* eslint-enable @typescript-eslint/no-unused-vars */ + /** + * Capitalize the first letter of a string. + * @param string The string to capitalize the first letter of. + * @returns The string with the first letter capitalized. + */ public capitalizeFirstLetter(string: string): string { return string.charAt(0)?.toUpperCase() + string.slice(1); } /** * Wait an amount in seconds. + * @param s The number of seconds to wait + * @returns A promise that resolves after the specified amount of seconds */ public async sleep(s: number) { return new Promise((resolve) => setTimeout(resolve, s * 1000)); } + /** + * 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 client.console.error(_.camelCase(context), `An error occurred:\n${error?.stack ?? (error as any)}`, false); await client.console.channelError({ @@ -553,24 +654,24 @@ export class BushClientUtil extends ClientUtil { if (!apiRes) return undefined; if (!apiRes.pronouns) throw new Error('apiRes.pronouns is undefined'); - return client.constants.pronounMapping[apiRes.pronouns]; + return client.constants.pronounMapping[apiRes.pronouns!]!; } - // modified from https://stackoverflow.com/questions/31054910/get-functions-methods-of-a-class - // answer by Bruno Grieder - public getMethods(_obj: any): string { + public getMethods(obj: Record<string, any>): string { + // modified from https://stackoverflow.com/questions/31054910/get-functions-methods-of-a-class + // answer by Bruno Grieder let props: string[] = []; - let obj: any = new Object(_obj); + let obj_: Record<string, any> = new Object(obj); do { - const l = Object.getOwnPropertyNames(obj) - .concat(Object.getOwnPropertySymbols(obj).map((s) => s.toString())) + 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 + 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 @@ -580,10 +681,10 @@ export class BushClientUtil extends ClientUtil { props = props.concat( l.map( (p) => - `${obj[p] && obj[p][Symbol.toStringTag] === 'AsyncFunction' ? 'async ' : ''}function ${p}(${ - reg.exec(obj[p].toString())?.[1] + `${obj_[p] && obj_[p][Symbol.toStringTag] === 'AsyncFunction' ? 'async ' : ''}function ${p}(${ + reg.exec(obj_[p].toString())?.[1] ? reg - .exec(obj[p].toString())?.[1] + .exec(obj_[p].toString())?.[1] .split(', ') .map((arg) => arg.split('=')[0].trim()) .join(', ') @@ -592,21 +693,13 @@ export class BushClientUtil extends ClientUtil { ) ); } while ( - (obj = Object.getPrototypeOf(obj)) && //walk-up the prototype chain - Object.getPrototypeOf(obj) //not the the Object prototype methods (hasOwnProperty, etc...) + (obj_ = Object.getPrototypeOf(obj_)) && //walk-up the prototype chain + Object.getPrototypeOf(obj_) //not the the Object prototype methods (hasOwnProperty, etc...) ); return props.join('\n'); } - /** - * Removes all characters in a string that are either control characters or change the direction of text etc. - */ - public sanitizeWtlAndControl(str: string) { - // eslint-disable-next-line no-control-regex - return str.replace(/[\u0000-\u001F\u007F-\u009F\u200B]/g, ''); - } - public async uploadImageToImgur(image: string) { const clientId = this.client.config.credentials.imgurClientId; @@ -667,6 +760,14 @@ export class BushClientUtil extends ClientUtil { : message.util.parsed?.prefix ?? client.config.prefix; } + public get deepFreeze() { + return deepLock; + } + + public static get deepFreeze() { + return deepLock; + } + public get arg() { return Arg; } @@ -686,6 +787,13 @@ export class BushClientUtil extends ClientUtil { } /** + * Discord.js's Util constants + */ + public get discordConstants() { + return DiscordConstants + } + + /** * discord-akairo's Util class */ public get akairo() { @@ -693,11 +801,11 @@ export class BushClientUtil extends ClientUtil { } } -interface hastebinRes { +interface HastebinRes { key: string; } -export interface uuidRes { +export interface UuidRes { uuid: string; username: string; username_history?: { username: string }[] | null; @@ -716,7 +824,12 @@ export interface uuidRes { created_at: string; } -interface MojangProfile { - username: string; - uuid: string; +export interface HasteResults { + url?: string; + error?: 'content too long' | 'substr' | 'unable to post'; +} + +export interface ParsedDuration { + duration: number | null; + contentWithoutTime: string | null; } diff --git a/src/lib/extensions/discord-akairo/BushCommand.ts b/src/lib/extensions/discord-akairo/BushCommand.ts index 1494aee..8872831 100644 --- a/src/lib/extensions/discord-akairo/BushCommand.ts +++ b/src/lib/extensions/discord-akairo/BushCommand.ts @@ -1,13 +1,23 @@ import { type BushClient, type BushCommandHandler, type BushMessage, type BushSlashMessage } from '#lib'; import { + AkairoApplicationCommandAutocompleteOption, + AkairoApplicationCommandChannelOptionData, + AkairoApplicationCommandChoicesData, + AkairoApplicationCommandNonOptionsData, + AkairoApplicationCommandNumericOptionData, + AkairoApplicationCommandOptionData, + AkairoApplicationCommandSubCommandData, + AkairoApplicationCommandSubGroupData, Command, + MissingPermissionSupplier, + SlashOption, + SlashResolveTypes, type ArgumentOptions, - type ArgumentPromptOptions, type ArgumentTypeCaster, type CommandOptions } from 'discord-akairo'; import { BaseArgumentType } from 'discord-akairo/dist/src/struct/commands/arguments/Argument'; -import { type PermissionResolvable, type Snowflake } from 'discord.js'; +import { ApplicationCommandOptionChoice, type PermissionResolvable, type Snowflake } from 'discord.js'; export type BaseBushArgumentType = | BaseArgumentType @@ -18,14 +28,76 @@ export type BaseBushArgumentType = | 'discordEmoji' | 'roleWithDuration' | 'abbreviatedNumber' - | 'globalUser'; + | 'globalUser' + | 'messageLink' export type BushArgumentType = BaseBushArgumentType | RegExp; -interface BaseBushArgumentOptions extends Omit<ArgumentOptions, 'type'> { +interface BaseBushArgumentOptions extends Omit<ArgumentOptions, 'type' | 'prompt'> { id: string; - description?: string; - prompt?: ArgumentPromptOptions; + description: string; + + /** + * The message sent for the prompt and the slash command description. + */ + prompt?: string; + + /** + * The message set for the retry prompt. + */ + retry?: string; + + /** + * Whether or not the argument is optional. + */ + optional?: boolean; + + /** + * The type used for slash commands. Set to false to disable this argument for slash commands. + */ + slashType: AkairoApplicationCommandOptionData['type'] | false; + + /** + * Allows you to get a discord resolved object + * + * ex. get the resolved member object when the type is `USER` + */ + slashResolve?: SlashResolveTypes; + + /** + * The choices of the option for the user to pick from + */ + choices?: ApplicationCommandOptionChoice[]; + + /** + * Whether the option is an autocomplete option + */ + autocomplete?: boolean; + + /** + * When the option type is channel, the allowed types of channels that can be selected + */ + channelTypes?: AkairoApplicationCommandChannelOptionData['channelTypes']; + + /** + * The minimum value for an `INTEGER` or `NUMBER` option + */ + minValue?: number; + + /** + * The maximum value for an `INTEGER` or `NUMBER` option + */ + maxValue?: number; + + /** + * Restrict this argument to only slash or only text commands. + */ + only?: 'slash' | 'text'; + + /** + * Readable type for the help command. + */ + readableType?: string; } export interface BushArgumentOptions extends BaseBushArgumentOptions { @@ -93,7 +165,7 @@ export interface CustomBushArgumentOptions extends BaseBushArgumentOptions { export type BushMissingPermissionSupplier = (message: BushMessage | BushSlashMessage) => Promise<any> | any; -export interface BushCommandOptions extends Omit<CommandOptions, 'userPermissions' | 'clientPermissions'> { +export interface BaseBushCommandOptions extends Omit<CommandOptions, 'userPermissions' | 'clientPermissions' | 'args'> { /** * Whether the command is hidden from the help command. */ @@ -109,12 +181,24 @@ export interface BushCommandOptions extends Omit<CommandOptions, 'userPermission */ restrictedGuilds?: Snowflake[]; - description: { - content: string; - usage: string[]; - examples: string[]; - }; + /** + * The description of the command. + */ + description: string; + + /** + * Show how to use the command. + */ + usage: string[]; + /** + * Examples for how to use the command. + */ + examples: string[]; + + /** + * The arguments for the command. + */ args?: BushArgumentOptions[] & CustomBushArgumentOptions[]; category: string; @@ -138,6 +222,33 @@ export interface BushCommandOptions extends Omit<CommandOptions, 'userPermission * Permissions required by the user to run this command. */ userPermissions: PermissionResolvable | PermissionResolvable[] | BushMissingPermissionSupplier; + + /** + * Restrict this argument to owners + */ + ownerOnly?: boolean; + + /** + * Restrict this argument to super users. + */ + superUserOnly?: boolean; + + /** + * Use instead of {@link BaseBushCommandOptions.args} when using argument generators or custom slashOptions + */ + helpArgs?: BushArgumentOptions[]; +} + +export type BushCommandOptions = Omit<BaseBushCommandOptions, 'helpArgs'> | Omit<BaseBushCommandOptions, 'args'>; + +export interface ArgsInfo { + id: string; + description: string; + optional?: boolean; + slashType: AkairoApplicationCommandOptionData['type'] | false; + slashResolve?: SlashResolveTypes; + only?: 'slash' | 'text'; + type: string; } export class BushCommand extends Command { @@ -145,18 +256,29 @@ export class BushCommand extends Command { public declare handler: BushCommandHandler; - public declare description: { - content: string; - usage: string[]; - examples: string[]; - }; + public declare description: string; + + /** + * Show how to use the command. + */ + public usage: string[]; + + /** + * Examples for how to use the command. + */ + public examples: string[]; /** - * The command's options + * The options sent to the constructor */ public options: BushCommandOptions; /** + * The options sent to the super call + */ + public parsedOptions: CommandOptions; + + /** * The channels the command is limited to run in. */ public restrictedChannels: Snowflake[] | undefined; @@ -181,24 +303,138 @@ export class BushCommand extends Command { */ public bypassChannelBlacklist: boolean; + /** + * Info about the arguments for the help command. + */ + public argsInfo?: ArgsInfo[]; + public constructor(id: string, options: BushCommandOptions) { - if (options.args && typeof options.args !== 'function') { - options.args.forEach((_, index: number) => { - if ('customType' in options.args![index]) { - // @ts-expect-error: shut - if (!options.args[index]['type']) options.args[index]['type'] = options.args[index]['customType']; - delete options.args![index]['customType']; + const options_ = options as BaseBushCommandOptions; + + if (options_.args && typeof options_.args !== 'function') { + options_.args.forEach((_, index: number) => { + if ('customType' in (options_.args?.[index] ?? {})) { + if (!options_.args![index]['type']) options_.args![index]['type'] = options_.args![index]['customType']! as any; + delete options_.args![index]['customType']; } }); } - // incompatible options - super(id, options as any); - this.options = options; - this.hidden = Boolean(options.hidden); - this.restrictedChannels = options.restrictedChannels; - this.restrictedGuilds = options.restrictedGuilds; - this.pseudo = Boolean(options.pseudo); - this.bypassChannelBlacklist = Boolean(options.bypassChannelBlacklist); + + const newOptions: CommandOptions = {}; + if ('aliases' in options_) newOptions.aliases = options_.aliases; + if ('args' in options_ && typeof options_.args === 'object') { + const newTextArgs: ArgumentOptions[] = []; + const newSlashArgs: SlashOption[] = []; + for (const arg of options_.args) { + if (arg.only !== 'slash' && !options_.slashOnly) { + const newArg: ArgumentOptions = {}; + if ('default' in arg) newArg.default = arg.default; + if ('description' in arg) newArg.description = arg.description; + if ('flag' in arg) newArg.flag = arg.flag; + if ('id' in arg) newArg.id = arg.id; + if ('index' in arg) newArg.index = arg.index; + if ('limit' in arg) newArg.limit = arg.limit; + if ('match' in arg) newArg.match = arg.match; + if ('modifyOtherwise' in arg) newArg.modifyOtherwise = arg.modifyOtherwise; + if ('multipleFlags' in arg) newArg.multipleFlags = arg.multipleFlags; + if ('otherwise' in arg) newArg.otherwise = arg.otherwise; + if ('prompt' in arg || 'retry' in arg || 'optional' in arg) { + newArg.prompt = {}; + if ('prompt' in arg) newArg.prompt.start = arg.prompt; + if ('retry' in arg) newArg.prompt.retry = arg.retry; + if ('optional' in arg) newArg.prompt.optional = arg.optional; + } + if ('type' in arg) newArg.type = arg.type; + if ('unordered' in arg) newArg.unordered = arg.unordered; + newTextArgs.push(newArg); + } + if ( + arg.only !== 'text' && + !('slashOptions' in options_) && + (options_.slash || options_.slashOnly) && + arg.slashType !== false + ) { + const newArg: { + [key in SlashOptionKeys]?: any; + } = { + name: arg.id, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + description: arg.prompt || arg.description || 'No description provided.', + type: arg.slashType + }; + if ('slashResolve' in arg) newArg.resolve = arg.slashResolve; + if ('autocomplete' in arg) newArg.autocomplete = arg.autocomplete; + if ('channelTypes' in arg) newArg.channelTypes = arg.channelTypes; + if ('choices' in arg) newArg.choices = arg.choices; + if ('minValue' in arg) newArg.minValue = arg.minValue; + if ('maxValue' in arg) newArg.maxValue = arg.maxValue; + newArg.required = 'optional' in arg ? !arg.optional : true; + newSlashArgs.push(newArg as SlashOption); + } + } + newOptions.args = newTextArgs; + newOptions.slashOptions = options_.slashOptions ?? newSlashArgs; + } + type perm = PermissionResolvable | PermissionResolvable[] | MissingPermissionSupplier; + + if ('argumentDefaults' in options_) newOptions.argumentDefaults = options_.argumentDefaults; + if ('before' in options_) newOptions.before = options_.before; + if ('channel' in options_) newOptions.channel = options_.channel; + if ('clientPermissions' in options_) newOptions.clientPermissions = options_.clientPermissions as perm; + if ('condition' in options_) newOptions.condition = options_.condition; + if ('cooldown' in options_) newOptions.cooldown = options_.cooldown; + if ('description' in options_) newOptions.description = options_.description; + if ('editable' in options_) newOptions.editable = options_.editable; + if ('flags' in options_) newOptions.flags = options_.flags; + if ('ignoreCooldown' in options_) newOptions.ignoreCooldown = options_.ignoreCooldown; + if ('ignorePermissions' in options_) newOptions.ignorePermissions = options_.ignorePermissions; + if ('lock' in options_) newOptions.lock = options_.lock; + if ('onlyNsfw' in options_) newOptions.onlyNsfw = options_.onlyNsfw; + if ('optionFlags' in options_) newOptions.optionFlags = options_.optionFlags; + if ('ownerOnly' in options_) newOptions.ownerOnly = options_.ownerOnly; + if ('prefix' in options_) newOptions.prefix = options_.prefix; + if ('quoted' in options_) newOptions.quoted = options_.quoted; + if ('ratelimit' in options_) newOptions.ratelimit = options_.ratelimit; + if ('regex' in options_) newOptions.regex = options_.regex; + if ('separator' in options_) newOptions.separator = options_.separator; + if ('slash' in options_) newOptions.slash = options_.slash; + if ('slashEphemeral' in options_) newOptions.slashEphemeral = options_.slashEphemeral; + if ('slashGuilds' in options_) newOptions.slashGuilds = options_.slashGuilds; + if ('slashOptions' in options_) newOptions.slashOptions = options_.slashOptions; + if ('superUserOnly' in options_) newOptions.superUserOnly = options_.superUserOnly; + if ('typing' in options_) newOptions.typing = options_.typing; + if ('userPermissions' in options_) newOptions.userPermissions = options_.userPermissions as perm; + + super(id, newOptions); + + if (options_.args || options_.helpArgs) { + const argsInfo: ArgsInfo[] = []; + + for (const arg of (options_.args ?? options_.helpArgs)!) { + argsInfo.push({ + id: arg.id, + description: arg.description, + optional: arg.optional, + slashType: arg.slashType, + slashResolve: arg.slashResolve, + only: arg.only, + type: (arg.readableType ?? arg.type) as string + }); + } + + this.argsInfo = argsInfo; + } + + this.description = options_.description; + this.usage = options_.usage; + this.examples = options_.examples; + this.options = options_; + this.parsedOptions = newOptions; + this.hidden = !!options_.hidden; + this.restrictedChannels = options_.restrictedChannels; + this.restrictedGuilds = options_.restrictedGuilds; + this.pseudo = !!options_.pseudo; + this.bypassChannelBlacklist = !!options_.bypassChannelBlacklist; } } @@ -206,3 +442,12 @@ export interface BushCommand { exec(message: BushMessage, args: any): any; exec(message: BushMessage | BushSlashMessage, args: any): any; } + +type SlashOptionKeys = + | keyof AkairoApplicationCommandSubGroupData + | keyof AkairoApplicationCommandNonOptionsData + | keyof AkairoApplicationCommandChannelOptionData + | keyof AkairoApplicationCommandChoicesData + | keyof AkairoApplicationCommandAutocompleteOption + | keyof AkairoApplicationCommandNumericOptionData + | keyof AkairoApplicationCommandSubCommandData; diff --git a/src/lib/extensions/discord.js/BushGuild.ts b/src/lib/extensions/discord.js/BushGuild.ts index e3b39d3..9c272ff 100644 --- a/src/lib/extensions/discord.js/BushGuild.ts +++ b/src/lib/extensions/discord.js/BushGuild.ts @@ -9,8 +9,9 @@ import type { GuildLogType, GuildModel } from '#lib'; -import { Guild, type MessageOptions, type UserResolvable } from 'discord.js'; +import { Guild, MessagePayload, type MessageOptions, type UserResolvable } from 'discord.js'; import type { RawGuildData } from 'discord.js/typings/rawDataTypes'; +import _ from 'lodash'; import { Moderation } from '../../common/Moderation.js'; import { Guild as GuildDB } from '../../models/Guild.js'; import { ModLogType } from '../../models/ModLog.js'; @@ -24,29 +25,52 @@ export class BushGuild extends Guild { super(client, data); } + /** + * Checks if the guild has a certain custom feature. + * @param feature The feature to check for + */ public async hasFeature(feature: GuildFeatures): Promise<boolean> { const features = await this.getSetting('enabledFeatures'); return features.includes(feature); } + /** + * Adds a custom feature to the guild. + * @param feature The feature to add + * @param moderator The moderator responsible for adding a feature + */ public async addFeature(feature: GuildFeatures, moderator?: BushGuildMember): Promise<GuildModel['enabledFeatures']> { const features = await this.getSetting('enabledFeatures'); const newFeatures = util.addOrRemoveFromArray('add', features, feature); return (await this.setSetting('enabledFeatures', newFeatures, moderator)).enabledFeatures; } + /** + * Removes a custom feature from the guild. + * @param feature The feature to remove + * @param moderator The moderator responsible for removing a feature + */ public async removeFeature(feature: GuildFeatures, moderator?: BushGuildMember): Promise<GuildModel['enabledFeatures']> { const features = await this.getSetting('enabledFeatures'); const newFeatures = util.addOrRemoveFromArray('remove', features, feature); return (await this.setSetting('enabledFeatures', newFeatures, moderator)).enabledFeatures; } + /** + * Makes a custom feature the opposite of what it was before + * @param feature The feature to toggle + * @param moderator The moderator responsible for toggling a feature + */ public async toggleFeature(feature: GuildFeatures, moderator?: BushGuildMember): Promise<GuildModel['enabledFeatures']> { return (await this.hasFeature(feature)) ? await this.removeFeature(feature, moderator) : await this.addFeature(feature, moderator); } + /** + * Fetches a custom setting for the guild + * @param setting The setting to get + */ public async getSetting<K extends keyof GuildModel>(setting: K): Promise<GuildModel[K]> { return ( client.cache.guilds.get(this.id)?.[setting] ?? @@ -54,6 +78,12 @@ export class BushGuild extends Guild { ); } + /** + * Sets a custom setting for the guild + * @param setting The setting to change + * @param value The value to change the setting to + * @param moderator The moderator to responsible for changing the setting + */ public async setSetting<K extends Exclude<keyof GuildModel, 'id'>>( setting: K, value: GuildDB[K], @@ -208,13 +238,25 @@ export class BushGuild extends Guild { } /** - * Sends a message to the guild's specified logging channel. + * Sends a message to the guild's specified logging channel + * @param logType The corresponding channel that the message will be sent to + * @param message The parameters for {@link BushTextChannel.send} */ - public async sendLogChannel(logType: GuildLogType, message: MessageOptions) { + public async sendLogChannel(logType: GuildLogType, message: string | MessagePayload | MessageOptions) { const logChannel = await this.getLogChannel(logType); if (!logChannel || logChannel.type !== 'GUILD_TEXT') return; if (!logChannel.permissionsFor(this.me!.id)?.has(['VIEW_CHANNEL', 'SEND_MESSAGES', 'EMBED_LINKS'])) return; return await logChannel.send(message).catch(() => null); } + + /** + * Sends a formatted error message in a guild's error log channel + * @param title The title of the error embed + * @param message The description of the error embed + */ + public async error(title: string, message: string) { + void client.console.info(_.camelCase(title), message.replace(/\*\*(.*?)\*\*/g, '<<$1>>')); + void this.sendLogChannel('error', { embeds: [{ title: title, description: message, color: util.colors.error }] }); + } } diff --git a/src/lib/extensions/global.d.ts b/src/lib/extensions/global.d.ts index 1a30056..8427873 100644 --- a/src/lib/extensions/global.d.ts +++ b/src/lib/extensions/global.d.ts @@ -4,4 +4,13 @@ declare global { var client: BushClient; var util: BushClientUtil; var __rootdir__: string; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface ReadonlyArray<T> { + includes<S, R extends `${Extract<S, string>}`>( + this: ReadonlyArray<R>, + searchElement: S, + fromIndex?: number + ): searchElement is R & S; + } } |