aboutsummaryrefslogtreecommitdiff
path: root/src/lib/common
diff options
context:
space:
mode:
authorIRONM00N <64110067+IRONM00N@users.noreply.github.com>2021-10-21 00:05:53 -0400
committerIRONM00N <64110067+IRONM00N@users.noreply.github.com>2021-10-21 00:05:53 -0400
commit166d7fdf24440db71311c2cda95697c06e7b8b36 (patch)
tree23b0400362b5f3035b156200eb634d202aa54741 /src/lib/common
parent08f33f7d450c8920afc3b9fb8886729547065313 (diff)
downloadtanzanite-166d7fdf24440db71311c2cda95697c06e7b8b36.tar.gz
tanzanite-166d7fdf24440db71311c2cda95697c06e7b8b36.tar.bz2
tanzanite-166d7fdf24440db71311c2cda95697c06e7b8b36.zip
Refactoring, rewrote ButtonPaginator, better permission handling + support for send messages in threads, optimizations, another scam link
Diffstat (limited to 'src/lib/common')
-rw-r--r--src/lib/common/ButtonPaginator.ts186
-rw-r--r--src/lib/common/DeleteButton.ts61
-rw-r--r--src/lib/common/Format.ts72
-rw-r--r--src/lib/common/autoMod.ts52
-rw-r--r--src/lib/common/moderation.ts19
-rw-r--r--src/lib/common/typings/BushInspectOptions.d.ts91
-rw-r--r--src/lib/common/typings/CodeBlockLang.d.ts310
-rw-r--r--src/lib/common/util/Arg.ts120
8 files changed, 871 insertions, 40 deletions
diff --git a/src/lib/common/ButtonPaginator.ts b/src/lib/common/ButtonPaginator.ts
new file mode 100644
index 0000000..c74f6ad
--- /dev/null
+++ b/src/lib/common/ButtonPaginator.ts
@@ -0,0 +1,186 @@
+import {
+ Constants,
+ MessageActionRow,
+ MessageButton,
+ MessageComponentInteraction,
+ MessageEmbed,
+ MessageEmbedOptions
+} from 'discord.js';
+import { BushMessage, BushSlashMessage } from '..';
+import { DeleteButton } from './DeleteButton';
+
+export class ButtonPaginator {
+ protected message: BushMessage | BushSlashMessage;
+ protected embeds: MessageEmbed[] | MessageEmbedOptions[];
+ protected text: string | null;
+ protected deleteOnExit: boolean;
+ protected curPage: number;
+ protected sentMessage: BushMessage | undefined;
+
+ /**
+ * Sends multiple embeds with controls to switch between them
+ * @param message - The message to respond to
+ * @param embeds - The embeds to switch between
+ * @param text - The text send with the embeds (optional)
+ * @param deleteOnExit - Whether to delete the message when the exit button is clicked (defaults to true)
+ * @param startOn - The page to start from (**not** the index)
+ */
+ public static async send(
+ message: BushMessage | BushSlashMessage,
+ embeds: MessageEmbed[] | MessageEmbedOptions[],
+ text: string | null = null,
+ deleteOnExit = true,
+ startOn = 1
+ ): Promise<void> {
+ // no need to paginate if there is only one page
+ if (embeds.length === 1) return DeleteButton.send(message, { embeds: embeds });
+
+ return await new ButtonPaginator(message, embeds, text, deleteOnExit, startOn).send();
+ }
+
+ protected get numPages(): number {
+ return this.embeds.length;
+ }
+
+ protected constructor(
+ message: BushMessage | BushSlashMessage,
+ embeds: MessageEmbed[] | MessageEmbedOptions[],
+ text: string | null,
+ deleteOnExit: boolean,
+ startOn: number
+ ) {
+ this.message = message;
+ this.embeds = embeds;
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ this.text = text || null;
+ this.deleteOnExit = deleteOnExit;
+ this.curPage = startOn - 1;
+
+ // add footers
+ for (let i = 0; i < embeds.length; i++) {
+ if (embeds[i] instanceof MessageEmbed) {
+ (embeds[i] as MessageEmbed).setFooter(`Page ${(i + 1).toLocaleString()}/${embeds.length.toLocaleString()}`);
+ } else {
+ (embeds[i] as MessageEmbedOptions).footer = {
+ text: `Page ${(i + 1).toLocaleString()}/${embeds.length.toLocaleString()}`
+ };
+ }
+ }
+ }
+
+ protected async send() {
+ this.sentMessage = (await this.message.util.reply({
+ content: this.text,
+ embeds: [this.embeds[this.curPage]],
+ components: [this.getPaginationRow()]
+ })) as BushMessage;
+
+ const collector = this.sentMessage.createMessageComponentCollector({
+ filter: (i) => i.customId.startsWith('paginate_') && i.message.id === this.sentMessage!.id,
+ time: 300000
+ });
+
+ collector.on('collect', (i) => void this.collect(i));
+ collector.on('end', () => void this.end());
+ }
+
+ protected async collect(interaction: MessageComponentInteraction) {
+ if (interaction.user.id !== this.message.author.id || !client.config.owners.includes(interaction.user.id))
+ return await interaction?.deferUpdate().catch(() => undefined);
+
+ switch (interaction.customId) {
+ case 'paginate_beginning':
+ this.curPage = 0;
+ return this.edit(interaction);
+ case 'paginate_back':
+ this.curPage--;
+ return await this.edit(interaction);
+ case 'paginate_stop':
+ if (this.deleteOnExit) {
+ await interaction.deferUpdate().catch(() => undefined);
+ return await this.sentMessage!.delete().catch(() => undefined);
+ } else {
+ return await interaction
+ ?.update({
+ content: `${this.text ? `${this.text}\n` : ''}Command closed by user.`,
+ embeds: [],
+ components: []
+ })
+ .catch(() => undefined);
+ }
+ case 'paginate_next':
+ this.curPage++;
+ return await this.edit(interaction);
+ case 'paginate_end':
+ this.curPage = this.embeds.length - 1;
+ return await this.edit(interaction);
+ }
+ }
+
+ protected async end() {
+ try {
+ return this.sentMessage!.edit({
+ content: this.text,
+ embeds: [this.embeds[this.curPage]],
+ components: [this.getPaginationRow(true)]
+ });
+ } catch (e) {
+ return undefined;
+ }
+ }
+
+ protected async edit(interaction: MessageComponentInteraction) {
+ try {
+ return interaction?.update({
+ content: this.text,
+ embeds: [this.embeds[this.curPage]],
+ components: [this.getPaginationRow()]
+ });
+ } catch (e) {
+ return undefined;
+ }
+ }
+
+ protected getPaginationRow(disableAll = false): MessageActionRow {
+ return new MessageActionRow().addComponents(
+ new MessageButton({
+ style: Constants.MessageButtonStyles.PRIMARY,
+ customId: 'paginate_beginning',
+ emoji: PaginateEmojis.BEGGING,
+ disabled: disableAll || this.curPage === 0
+ }),
+ new MessageButton({
+ style: Constants.MessageButtonStyles.PRIMARY,
+ customId: 'paginate_back',
+ emoji: PaginateEmojis.BACK,
+ disabled: disableAll || this.curPage === 0
+ }),
+ new MessageButton({
+ style: Constants.MessageButtonStyles.PRIMARY,
+ customId: 'paginate_stop',
+ emoji: PaginateEmojis.STOP,
+ disabled: disableAll
+ }),
+ new MessageButton({
+ style: Constants.MessageButtonStyles.PRIMARY,
+ customId: 'paginate_next',
+ emoji: PaginateEmojis.FORWARD,
+ disabled: disableAll || this.curPage === this.numPages - 1
+ }),
+ new MessageButton({
+ style: Constants.MessageButtonStyles.PRIMARY,
+ customId: 'paginate_end',
+ emoji: PaginateEmojis.END,
+ disabled: disableAll || this.curPage === this.numPages - 1
+ })
+ );
+ }
+}
+
+export const enum PaginateEmojis {
+ BEGGING = '853667381335162910',
+ BACK = '853667410203770881',
+ STOP = '853667471110570034',
+ FORWARD = '853667492680564747',
+ END = '853667514915225640'
+}
diff --git a/src/lib/common/DeleteButton.ts b/src/lib/common/DeleteButton.ts
new file mode 100644
index 0000000..7d2e41b
--- /dev/null
+++ b/src/lib/common/DeleteButton.ts
@@ -0,0 +1,61 @@
+import { Constants, MessageActionRow, MessageButton, MessageComponentInteraction, MessageOptions } from 'discord.js';
+import { BushMessage, BushSlashMessage } from '..';
+import { PaginateEmojis } from './ButtonPaginator';
+
+export class DeleteButton {
+ protected messageOptions: MessageOptions;
+ protected message: BushMessage | BushSlashMessage;
+
+ /**
+ * Sends a message with a button for the user to delete it.
+ * @param message - The message to respond to
+ * @param options - The send message options
+ */
+ static async send(message: BushMessage | BushSlashMessage, options: Omit<MessageOptions, 'components'>) {
+ return new DeleteButton(message, options).send();
+ }
+
+ protected constructor(message: BushMessage | BushSlashMessage, options: MessageOptions) {
+ this.message = message;
+ this.messageOptions = options;
+ }
+
+ protected async send() {
+ this.updateComponents();
+
+ const msg = (await this.message.util.reply(this.messageOptions)) as BushMessage;
+
+ const collector = msg.createMessageComponentCollector({
+ filter: (interaction) => interaction.customId == 'paginate__stop' && interaction.message.id == msg.id,
+ time: 300000
+ });
+
+ collector.on('collect', async (interaction: MessageComponentInteraction) => {
+ await interaction.deferUpdate().catch(() => undefined);
+ if (interaction.user.id == this.message.author.id || client.config.owners.includes(interaction.user.id)) {
+ if (msg.deletable && !msg.deleted) await msg.delete();
+ }
+ });
+
+ collector.on('end', async () => {
+ this.updateComponents(true, true);
+ await msg.edit(this.messageOptions).catch(() => undefined);
+ });
+ }
+
+ protected updateComponents(edit = false, disable = false): void {
+ this.messageOptions.components = [
+ new MessageActionRow().addComponents(
+ new MessageButton({
+ style: Constants.MessageButtonStyles.PRIMARY,
+ customId: 'paginate__stop',
+ emoji: PaginateEmojis.STOP,
+ disabled: disable
+ })
+ )
+ ];
+ if (edit) {
+ this.messageOptions.reply = undefined;
+ }
+ }
+}
diff --git a/src/lib/common/Format.ts b/src/lib/common/Format.ts
new file mode 100644
index 0000000..ba1ee9f
--- /dev/null
+++ b/src/lib/common/Format.ts
@@ -0,0 +1,72 @@
+import { Formatters, Util } from 'discord.js';
+import { CodeBlockLang } from './typings/CodeBlockLang';
+
+/**
+ * Formats and escapes content for formatting
+ */
+export class Format {
+ /**
+ * Wraps the content inside a codeblock with no language.
+ * @param content The content to wrap.
+ */
+ public static codeBlock(content: string): string;
+ /**
+ * Wraps the content inside a codeblock with the specified language.
+ * @param language The language for the codeblock.
+ * @param content The content to wrap.
+ */
+ public static codeBlock(language: CodeBlockLang, content: string): string;
+ public static codeBlock(languageOrContent: string, content?: string): string {
+ return typeof content === 'undefined'
+ ? Formatters.codeBlock(Util.escapeCodeBlock(languageOrContent))
+ : Formatters.codeBlock(languageOrContent, Util.escapeCodeBlock(languageOrContent));
+ }
+
+ /**
+ * Wraps the content inside \`backticks\`, which formats it as inline code.
+ * @param content The content to wrap.
+ */
+ public static inlineCode(content: string): string {
+ return Formatters.inlineCode(Util.escapeInlineCode(content));
+ }
+
+ /**
+ * Formats the content into italic text.
+ * @param content The content to wrap.
+ */
+ public static italic(content: string): string {
+ return Formatters.italic(Util.escapeItalic(content));
+ }
+
+ /**
+ * Formats the content into bold text.
+ * @param content The content to wrap.
+ */
+ public static bold(content: string): string {
+ return Formatters.bold(Util.escapeBold(content));
+ }
+
+ /**
+ * Formats the content into underscored text.
+ * @param content The content to wrap.
+ */
+ public static underscore(content: string): string {
+ return Formatters.underscore(Util.escapeUnderline(content));
+ }
+
+ /**
+ * Formats the content into strike-through text.
+ * @param content The content to wrap.
+ */
+ public static strikethrough(content: string): string {
+ return Formatters.strikethrough(Util.escapeStrikethrough(content));
+ }
+
+ /**
+ * Wraps the content inside spoiler (hidden text).
+ * @param content The content to wrap.
+ */
+ public static spoiler(content: string): string {
+ return Formatters.spoiler(Util.escapeSpoiler(content));
+ }
+}
diff --git a/src/lib/common/autoMod.ts b/src/lib/common/autoMod.ts
index 0bdbebf..312beb3 100644
--- a/src/lib/common/autoMod.ts
+++ b/src/lib/common/autoMod.ts
@@ -1,17 +1,17 @@
-import { Formatters, MessageActionRow, MessageButton, MessageEmbed, TextChannel } from 'discord.js';
-import badLinksArray from '../../lib/badlinks';
-import badLinksSecretArray from '../../lib/badlinks-secret'; // I cannot make this public so just make a new file that export defaults an empty array
-import badWords from '../../lib/badwords';
+import { GuildMember, MessageActionRow, MessageButton, MessageEmbed, TextChannel } from 'discord.js';
+import badLinksArray from '../badlinks';
+import badLinksSecretArray from '../badlinks-secret'; // I cannot make this public so just make a new file that export defaults an empty array
+import badWords from '../badwords';
import { BushButtonInteraction } from '../extensions/discord.js/BushButtonInteraction';
-import { BushGuildMember } from '../extensions/discord.js/BushGuildMember';
import { BushMessage } from '../extensions/discord.js/BushMessage';
-import { Moderation } from './moderation';
+import { Moderation } from './Moderation';
export class AutoMod {
private message: BushMessage;
public constructor(message: BushMessage) {
this.message = message;
+ if (message.author.id === client.user?.id) return;
void this.handle();
}
@@ -21,17 +21,10 @@ export class AutoMod {
const customAutomodPhrases = (await this.message.guild.getSetting('autoModPhases')) ?? {};
const badLinks: BadWords = {};
- const badLinksSecret: BadWords = {};
- badLinksArray.forEach((link) => {
- badLinks[link] = {
- severity: Severity.PERM_MUTE,
- ignoreSpaces: true,
- ignoreCapitalization: true,
- reason: 'malicious link'
- };
- });
- badLinksSecretArray.forEach((link) => {
+ const uniqueLinks = [...new Set([...badLinksArray, ...badLinksSecretArray])];
+
+ uniqueLinks.forEach((link) => {
badLinks[link] = {
severity: Severity.PERM_MUTE,
ignoreSpaces: true,
@@ -43,9 +36,7 @@ export class AutoMod {
const result = {
...this.checkWords(customAutomodPhrases),
...this.checkWords((await this.message.guild.hasFeature('excludeDefaultAutomod')) ? {} : badWords),
- ...this.checkWords(
- (await this.message.guild.hasFeature('excludeAutomodScamLinks')) ? {} : { ...badLinks, ...badLinksSecret }
- )
+ ...this.checkWords((await this.message.guild.hasFeature('excludeAutomodScamLinks')) ? {} : badLinks)
};
if (Object.keys(result).length === 0) return;
@@ -59,9 +50,7 @@ export class AutoMod {
embeds: [
{
title: 'AutoMod Error',
- description: `Unable to find severity information for ${Formatters.inlineCode(
- util.discord.escapeInlineCode(highestOffence.word)
- )}`,
+ description: `Unable to find severity information for ${util.format.inlineCode(highestOffence.word)}`,
color: util.colors.error
}
]
@@ -128,7 +117,7 @@ export class AutoMod {
break;
}
default: {
- throw new Error('Invalid severity');
+ throw new Error(`Invalid severity: ${highestOffence.severity}`);
}
}
@@ -163,8 +152,8 @@ export class AutoMod {
.setDescription(
`**User:** ${this.message.author} (${this.message.author.tag})\n**Sent From**: <#${
this.message.channel.id
- }> [Jump to context](${this.message.url})\n**Blacklisted Words:** ${util
- .surroundArray(Object.keys(offences), '`')
+ }> [Jump to context](${this.message.url})\n**Blacklisted Words:** ${Object.keys(offences)
+ .map((key) => `\`${key}\``)
.join(', ')}`
)
.addField('Message Content', `${await util.codeblock(this.message.content, 1024)}`)
@@ -194,12 +183,13 @@ export class AutoMod {
const [action, userId, reason] = interaction.customId.replace('automod;', '').split(';');
switch (action) {
case 'ban': {
- const check = await Moderation.permissionCheck(
- interaction.member as BushGuildMember,
- interaction.guild!.members.cache.get(userId)!,
- 'ban',
- true
- );
+ const victim = await interaction.guild!.members.fetch(userId);
+ const moderator =
+ interaction.member instanceof GuildMember
+ ? interaction.member
+ : await interaction.guild!.members.fetch(interaction.user.id);
+
+ const check = victim ? await Moderation.permissionCheck(moderator, victim, 'ban', true) : true;
if (check !== true)
return interaction.reply({
diff --git a/src/lib/common/moderation.ts b/src/lib/common/moderation.ts
index c8779fc..29d66fa 100644
--- a/src/lib/common/moderation.ts
+++ b/src/lib/common/moderation.ts
@@ -123,7 +123,7 @@ export class Moderation {
const expires = options.duration ? new Date(+new Date() + options.duration ?? 0) : undefined;
const user = (await util.resolveNonCachedUser(options.user))!.id;
const guild = client.guilds.resolveId(options.guild)!;
- const type = this.#findTypeEnum(options.type)!;
+ const type = this.findTypeEnum(options.type)!;
const entry = ActivePunishment.build(
options.extraInfo
@@ -144,7 +144,7 @@ export class Moderation {
}): Promise<boolean> {
const user = await util.resolveNonCachedUser(options.user);
const guild = client.guilds.resolveId(options.guild);
- const type = this.#findTypeEnum(options.type);
+ const type = this.findTypeEnum(options.type);
if (!user || !guild) return false;
@@ -160,18 +160,19 @@ export class Moderation {
success = false;
});
if (entries) {
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
- entries.forEach(async (entry) => {
- await entry.destroy().catch(async (e) => {
+ const promises = entries.map(async (entry) =>
+ entry.destroy().catch(async (e) => {
await util.handleError('removePunishmentEntry', e);
- });
- success = false;
- });
+ success = false;
+ })
+ );
+
+ await Promise.all(promises);
}
return success;
}
- static #findTypeEnum(type: 'mute' | 'ban' | 'role' | 'block') {
+ private static findTypeEnum(type: 'mute' | 'ban' | 'role' | 'block') {
const typeMap = {
['mute']: ActivePunishmentType.MUTE,
['ban']: ActivePunishmentType.BAN,
diff --git a/src/lib/common/typings/BushInspectOptions.d.ts b/src/lib/common/typings/BushInspectOptions.d.ts
new file mode 100644
index 0000000..c2a2360
--- /dev/null
+++ b/src/lib/common/typings/BushInspectOptions.d.ts
@@ -0,0 +1,91 @@
+import { InspectOptions } from 'util';
+
+/**
+ * {@link https://nodejs.org/api/util.html#util_util_inspect_object_options}
+ */
+export interface BushInspectOptions extends InspectOptions {
+ /**
+ * If `true`, object's non-enumerable symbols and properties are included in the
+ * formatted result. [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) and [`WeakSet`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) entries are also included as well as
+ * user defined prototype properties (excluding method properties).
+ *
+ * **Default**: `false`.
+ */
+ showHidden?: boolean | undefined;
+ /**
+ * Specifies the number of times to recurse while formatting `object`. This is useful
+ * for inspecting large objects. To recurse up to the maximum call stack size pass
+ * `Infinity` or `null`.
+ *
+ * **Default**: `2`.
+ */
+ depth?: number | null | undefined;
+ /**
+ * If `true`, the output is styled with ANSI color codes. Colors are customizable. See [Customizing util.inspect colors](https://nodejs.org/api/util.html#util_customizing_util_inspect_colors).
+ *
+ * **Default**: `false`.
+ */
+ colors?: boolean | undefined;
+ /**
+ * If `false`, `[util.inspect.custom](depth, opts)` functions are not invoked.
+ *
+ * **Default**: `true`.
+ */
+ customInspect?: boolean | undefined;
+ /**
+ * If `true`, `Proxy` inspection includes the [`target` and `handler`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#Terminology) objects.
+ *
+ * **Default**: `false`.
+ */
+ showProxy?: boolean | undefined;
+ /**
+ * Specifies the maximum number of `Array`, [`TypedArray`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray), [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) and
+ * [`WeakSet`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) elements to include when formatting. Set to `null` or `Infinity` to
+ * show all elements. Set to `0` or negative to show no elements.
+ *
+ * **Default**: `100`.
+ */
+ maxArrayLength?: number | null | undefined;
+ /**
+ * Specifies the maximum number of characters to include when formatting. Set to
+ * `null` or `Infinity` to show all elements. Set to `0` or negative to show no
+ * characters.
+ *
+ * **Default**: `10000`.
+ */
+ maxStringLength?: number | null | undefined;
+ /**
+ * The length at which input values are split across multiple lines. Set to
+ * `Infinity` to format the input as a single line (in combination with compact set
+ * to `true` or any number >= `1`).
+ *
+ * **Default**: `80`.
+ */
+ breakLength?: number | undefined;
+ /**
+ * Setting this to `false` causes each object key to be displayed on a new line. It
+ * will break on new lines in text that is longer than `breakLength`. If set to a
+ * number, the most `n` inner elements are united on a single line as long as all
+ * properties fit into `breakLength`. Short array elements are also grouped together.
+ *
+ * **Default**: `3`
+ */
+ compact?: boolean | number | undefined;
+ /**
+ * If set to `true` or a function, all properties of an object, and `Set` and `Map`
+ * entries are sorted in the resulting string. If set to `true` the [default sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) is used.
+ * If set to a function, it is used as a [compare function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#parameters).
+ *
+ * **Default**: `false`.
+ */
+ sorted?: boolean | ((a: string, b: string) => number) | undefined;
+ /**
+ * If set to `true`, getters are inspected. If set to `'get'`, only getters without a
+ * corresponding setter are inspected. If set to `'set'`, only getters with a
+ * corresponding setter are inspected. This might cause side effects depending on
+ * the getter function.
+ *
+ * **Default**: `false`.
+ */
+ getters?: 'get' | 'set' | boolean | undefined;
+}
diff --git a/src/lib/common/typings/CodeBlockLang.d.ts b/src/lib/common/typings/CodeBlockLang.d.ts
new file mode 100644
index 0000000..5a1aeba
--- /dev/null
+++ b/src/lib/common/typings/CodeBlockLang.d.ts
@@ -0,0 +1,310 @@
+export type CodeBlockLang =
+ | '1c'
+ | 'abnf'
+ | 'accesslog'
+ | 'actionscript'
+ | 'ada'
+ | 'arduino'
+ | 'ino'
+ | 'armasm'
+ | 'arm'
+ | 'avrasm'
+ | 'actionscript'
+ | 'as'
+ | 'angelscript'
+ | 'asc'
+ | 'apache'
+ | 'apacheconf'
+ | 'applescript'
+ | 'osascript'
+ | 'arcade'
+ | 'asciidoc'
+ | 'adoc'
+ | 'aspectj'
+ | 'autohotkey'
+ | 'autoit'
+ | 'awk'
+ | 'mawk'
+ | 'nawk'
+ | 'gawk'
+ | 'bash'
+ | 'sh'
+ | 'zsh'
+ | 'basic'
+ | 'bnf'
+ | 'brainfuck'
+ | 'bf'
+ | 'csharp'
+ | 'cs'
+ | 'c'
+ | 'h'
+ | 'cpp'
+ | 'hpp'
+ | 'cc'
+ | 'hh'
+ | 'c++'
+ | 'h++'
+ | 'cxx'
+ | 'hxx'
+ | 'cal'
+ | 'cos'
+ | 'cls'
+ | 'cmake'
+ | 'cmake.in'
+ | 'coq'
+ | 'csp'
+ | 'css'
+ | 'capnproto'
+ | 'capnp'
+ | 'clojure'
+ | 'clj'
+ | 'coffeescript'
+ | 'coffee'
+ | 'cson'
+ | 'iced'
+ | 'crmsh'
+ | 'crm'
+ | 'pcmk'
+ | 'crystal'
+ | 'cr'
+ | 'd'
+ | 'dns'
+ | 'zone'
+ | 'bind'
+ | 'dos'
+ | 'bat'
+ | 'cmd'
+ | 'dart'
+ | 'dpr'
+ | 'dfm'
+ | 'pas'
+ | 'pascal'
+ | 'diff'
+ | 'patch'
+ | 'django'
+ | 'jinja'
+ | 'dockerfile'
+ | 'docker'
+ | 'dsconfig'
+ | 'dts'
+ | 'dust'
+ | 'dst'
+ | 'ebnf'
+ | 'elixir'
+ | 'elm'
+ | 'erlang'
+ | 'erl'
+ | 'excel'
+ | 'xls'
+ | 'xlsx'
+ | 'fsharp'
+ | 'fs'
+ | 'fix'
+ | 'fortran'
+ | 'f90'
+ | 'f95'
+ | 'gcode'
+ | 'nc'
+ | 'gams'
+ | 'gms'
+ | 'gauss'
+ | 'gss'
+ | 'gherkin'
+ | 'go'
+ | 'golang'
+ | 'golo'
+ | 'gololang'
+ | 'gradle'
+ | 'groovy'
+ | 'xml'
+ | 'html'
+ | 'xhtml'
+ | 'rss'
+ | 'atom'
+ | 'xjb'
+ | 'xsd'
+ | 'xsl'
+ | 'plist'
+ | 'svg'
+ | 'http'
+ | 'https'
+ | 'haml'
+ | 'handlebars'
+ | 'hbs'
+ | 'html.hbs'
+ | 'html.handlebars'
+ | 'haskell'
+ | 'hs'
+ | 'haxe'
+ | 'hx'
+ | 'hlsl'
+ | 'hy'
+ | 'hylang'
+ | 'ini'
+ | 'toml'
+ | 'inform7'
+ | 'i7'
+ | 'irpf90'
+ | 'json'
+ | 'java'
+ | 'jsp'
+ | 'javascript'
+ | 'js'
+ | 'jsx'
+ | 'julia'
+ | 'julia-repl'
+ | 'kotlin'
+ | 'kt'
+ | 'tex'
+ | 'leaf'
+ | 'lasso'
+ | 'ls'
+ | 'lassoscript'
+ | 'less'
+ | 'ldif'
+ | 'lisp'
+ | 'livecodeserver'
+ | 'livescript'
+ | 'ls'
+ | 'lua'
+ | 'makefile'
+ | 'mk'
+ | 'mak'
+ | 'make'
+ | 'markdown'
+ | 'md'
+ | 'mkdown'
+ | 'mkd'
+ | 'mathematica'
+ | 'mma'
+ | 'wl'
+ | 'matlab'
+ | 'maxima'
+ | 'mel'
+ | 'mercury'
+ | 'mizar'
+ | 'mojolicious'
+ | 'monkey'
+ | 'moonscript'
+ | 'moon'
+ | 'n1ql'
+ | 'nsis'
+ | 'nginx'
+ | 'nginxconf'
+ | 'nim'
+ | 'nimrod'
+ | 'nix'
+ | 'ocaml'
+ | 'ml'
+ | 'objectivec'
+ | 'mm'
+ | 'objc'
+ | 'obj-c'
+ | 'obj-c++'
+ | 'objective-c++'
+ | 'glsl'
+ | 'openscad'
+ | 'scad'
+ | 'ruleslanguage'
+ | 'oxygene'
+ | 'pf'
+ | 'pf.conf'
+ | 'php'
+ | 'parser3'
+ | 'perl'
+ | 'pl'
+ | 'pm'
+ | 'plaintext'
+ | 'txt'
+ | 'text'
+ | 'pony'
+ | 'pgsql'
+ | 'postgres'
+ | 'postgresql'
+ | 'powershell'
+ | 'ps'
+ | 'ps1'
+ | 'processing'
+ | 'prolog'
+ | 'properties'
+ | 'protobuf'
+ | 'puppet'
+ | 'pp'
+ | 'python'
+ | 'py'
+ | 'gyp'
+ | 'profile'
+ | 'python-repl'
+ | 'pycon'
+ | 'k'
+ | 'kdb'
+ | 'qml'
+ | 'r'
+ | 'reasonml'
+ | 're'
+ | 'rib'
+ | 'rsl'
+ | 'graph'
+ | 'instances'
+ | 'ruby'
+ | 'rb'
+ | 'gemspec'
+ | 'podspec'
+ | 'thor'
+ | 'irb'
+ | 'rust'
+ | 'rs'
+ | 'sas'
+ | 'scss'
+ | 'sql'
+ | 'p21'
+ | 'step'
+ | 'stp'
+ | 'scala'
+ | 'scheme'
+ | 'scilab'
+ | 'sci'
+ | 'shell'
+ | 'console'
+ | 'smali'
+ | 'smalltalk'
+ | 'st'
+ | 'sml'
+ | 'ml'
+ | 'stan'
+ | 'stanfuncs'
+ | 'stata'
+ | 'stylus'
+ | 'styl'
+ | 'subunit'
+ | 'swift'
+ | 'tcl'
+ | 'tk'
+ | 'tap'
+ | 'thrift'
+ | 'tp'
+ | 'twig'
+ | 'craftcms'
+ | 'typescript'
+ | 'ts'
+ | 'vbnet'
+ | 'vb'
+ | 'vbscript'
+ | 'vbs'
+ | 'vhdl'
+ | 'vala'
+ | 'verilog'
+ | 'v'
+ | 'vim'
+ | 'axapta'
+ | 'x++'
+ | 'x86asm'
+ | 'xl'
+ | 'tao'
+ | 'xquery'
+ | 'xpath'
+ | 'xq'
+ | 'yml'
+ | 'yaml'
+ | 'zephir'
+ | 'zep';
diff --git a/src/lib/common/util/Arg.ts b/src/lib/common/util/Arg.ts
new file mode 100644
index 0000000..84d5aeb
--- /dev/null
+++ b/src/lib/common/util/Arg.ts
@@ -0,0 +1,120 @@
+import { Argument, ArgumentTypeCaster, Flag, ParsedValuePredicate, TypeResolver } from 'discord-akairo';
+import { Message } from 'discord.js';
+import { BushArgumentType } from '../..';
+
+export class Arg {
+ /**
+ * Casts a phrase to this argument's type.
+ * @param type - The type to cast to.
+ * @param resolver - The type resolver.
+ * @param message - Message that called the command.
+ * @param phrase - Phrase to process.
+ */
+ public static cast(type: BushArgumentType, resolver: TypeResolver, message: Message, phrase: string): Promise<any> {
+ return Argument.cast(type, resolver, message, phrase);
+ }
+
+ /**
+ * Creates a type that is the left-to-right composition of the given types.
+ * If any of the types fails, the entire composition fails.
+ * @param types - Types to use.
+ */
+ public static compose(...types: BushArgumentType[]): ArgumentTypeCaster {
+ return Argument.compose(...types);
+ }
+
+ /**
+ * Creates a type that is the left-to-right composition of the given types.
+ * If any of the types fails, the composition still continues with the failure passed on.
+ * @param types - Types to use.
+ */
+ public static composeWithFailure(...types: BushArgumentType[]): ArgumentTypeCaster {
+ return Argument.composeWithFailure(...types);
+ }
+
+ /**
+ * Checks if something is null, undefined, or a fail flag.
+ * @param value - Value to check.
+ */
+ public static isFailure(value: any): value is null | undefined | (Flag & { value: any }) {
+ return Argument.isFailure(value);
+ }
+
+ /**
+ * Creates a type from multiple types (product type).
+ * Only inputs where each type resolves with a non-void value are valid.
+ * @param types - Types to use.
+ */
+ public static product(...types: BushArgumentType[]): ArgumentTypeCaster {
+ return Argument.product(...types);
+ }
+
+ /**
+ * Creates a type where the parsed value must be within a range.
+ * @param type - The type to use.
+ * @param min - Minimum value.
+ * @param max - Maximum value.
+ * @param inclusive - Whether or not to be inclusive on the upper bound.
+ */
+ public static range(type: BushArgumentType, min: number, max: number, inclusive?: boolean): ArgumentTypeCaster {
+ return Argument.range(type, min, max, inclusive);
+ }
+
+ /**
+ * Creates a type that parses as normal but also tags it with some data.
+ * Result is in an object `{ tag, value }` and wrapped in `Flag.fail` when failed.
+ * @param type - The type to use.
+ * @param tag - Tag to add. Defaults to the `type` argument, so useful if it is a string.
+ */
+ public static tagged(type: BushArgumentType, tag?: any): ArgumentTypeCaster {
+ return Argument.tagged(type, tag);
+ }
+
+ /**
+ * Creates a type from multiple types (union type).
+ * The first type that resolves to a non-void value is used.
+ * Each type will also be tagged using `tagged` with themselves.
+ * @param types - Types to use.
+ */
+ public static taggedUnion(...types: BushArgumentType[]): ArgumentTypeCaster {
+ return Argument.taggedUnion(...types);
+ }
+
+ /**
+ * Creates a type that parses as normal but also tags it with some data and carries the original input.
+ * Result is in an object `{ tag, input, value }` and wrapped in `Flag.fail` when failed.
+ * @param type - The type to use.
+ * @param tag - Tag to add. Defaults to the `type` argument, so useful if it is a string.
+ */
+ public static taggedWithInput(type: BushArgumentType, tag?: any): ArgumentTypeCaster {
+ return Argument.taggedWithInput(type, tag);
+ }
+
+ /**
+ * Creates a type from multiple types (union type).
+ * The first type that resolves to a non-void value is used.
+ * @param types - Types to use.
+ */
+ public static union(...types: BushArgumentType[]): ArgumentTypeCaster {
+ return Argument.union(...types);
+ }
+
+ /**
+ * Creates a type with extra validation.
+ * If the predicate is not true, the value is considered invalid.
+ * @param type - The type to use.
+ * @param predicate - The predicate function.
+ */
+ public static validate(type: BushArgumentType, predicate: ParsedValuePredicate): ArgumentTypeCaster {
+ return Argument.validate(type, predicate);
+ }
+
+ /**
+ * Creates a type that parses as normal but also carries the original input.
+ * Result is in an object `{ input, value }` and wrapped in `Flag.fail` when failed.
+ * @param type - The type to use.
+ */
+ public static withInput(type: BushArgumentType): ArgumentTypeCaster {
+ return Argument.withInput(type);
+ }
+}