aboutsummaryrefslogtreecommitdiff
path: root/src/lib/extensions
diff options
context:
space:
mode:
authorIRONM00N <64110067+IRONM00N@users.noreply.github.com>2021-11-28 09:27:41 -0500
committerIRONM00N <64110067+IRONM00N@users.noreply.github.com>2021-11-28 09:27:41 -0500
commit453683b57b8ff013ff25e2aaa4aa1d2e047edcb7 (patch)
tree8b98d2f30dbb6a8448602446cfacf9091667cc33 /src/lib/extensions
parentde4c3dcaf172804d34ae708be1ed3e75af42f4d5 (diff)
downloadtanzanite-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.ts4
-rw-r--r--src/lib/extensions/discord-akairo/BushClientUtil.ts283
-rw-r--r--src/lib/extensions/discord-akairo/BushCommand.ts309
-rw-r--r--src/lib/extensions/discord.js/BushGuild.ts48
-rw-r--r--src/lib/extensions/global.d.ts9
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;
+ }
}