import { BaseCommandName } from '../types/BaseCommandName'; import { GuildStorage } from '../types/GuildStorage'; import { Message } from '../types/Message'; import { Command } from '../command/Command'; import { Client } from '../client/Client'; /** * Utility class containing handy static methods that can * be used anywhere * @module Util */ export class Util { /** * Tangible representation of all base command names * @static * @name baseCommandNames * @type {BaseCommandName[]} */ public static baseCommandNames: BaseCommandName[] = require('./static/baseCommandNames.json'); /** * Return whether or not a command was called in the given * message, the called command, the prefix used to call the * command, and the name or alias of the command used to call it. * >Returns `[false, null, null, null]` if no command was called * @static * @method wasCommandCalled * @param {Message} message Message to check * @returns {Tuple} */ public static async wasCommandCalled(message: Message): Promise<[boolean, Command, string, string]> { type CommandCallData = [boolean, Command, string, string]; const client: Client = message.client; const dm: boolean = message.channel.type !== 'text'; const negative: CommandCallData = [false, null, null, null]; const prefixes: string[] = [ `<@${client.user.id}>`, `<@!${client.user.id}>` ]; const guildStorage: GuildStorage = !dm ? message.guild.storage || client.storage.guilds.get(message.guild.id) : null; if (!dm) prefixes.push(await guildStorage.settings.get('prefix')); else prefixes.push(await client.storage.get('defaultGuildSettings.prefix')); let prefix: string = prefixes.find(a => message.content.trim().startsWith(a)); if (dm && typeof prefix === 'undefined') prefix = ''; if (typeof prefix === 'undefined' && !dm) return negative; const commandName: string = message.content.trim().slice(prefix.length).trim().split(' ')[0]; const command: Command = client.commands.findByNameOrAlias(commandName); if (!command) return negative; if (command.disabled) return negative; return [true, command, prefix, commandName]; } /** * Pads the right side of a string with spaces to the given length * @static * @method padRight * @param {string} text Text to pad * @param {number} length Length to pad to * @returns {string} */ public static padRight(text: string, length: number): string { let pad: number = Math.max(0, Math.min(length, length - text.length)); return `${text}${' '.repeat(pad)}`; } /** * Returns the given string lowercased with any non * alphanumeric chars removed * @static * @method normalize * @param {string} text Text to normalize * @returns {string} */ public static normalize(text: string): string { return text.toLowerCase().replace(/[^a-z0-9]+/g, ''); } /** * Assigns the given value along the given nested path within * the provided initial object * @static * @method assignNestedValue * @param {any} obj Object to assign to * @param {string[]} path Nested path to follow within the object * @param {any} value Value to assign within the object * @returns {void} */ public static assignNestedValue(obj: any, path: string[], value: any): void { if (typeof obj !== 'object' || obj instanceof Array) throw new Error(`Initial input of type '${typeof obj}' is not valid for nested assignment`); if (path.length === 0) throw new Error('Missing nested assignment path'); let first: string = path.shift(); if (typeof obj[first] === 'undefined') obj[first] = {}; if (path.length > 1 && (typeof obj[first] !== 'object' || obj[first] instanceof Array)) throw new Error(`Target '${first}' is not valid for nested assignment.`); if (path.length === 0) obj[first] = value; else Util.assignNestedValue(obj[first], path, value); } /** * Remove a value from within an object along a nested path * @static * @method removeNestedValue * @param {any} obj Object to remove from * @param {string[]} path Nested path to follow within the object * @returns {void} */ public static removeNestedValue(obj: any, path: string[]): void { if (typeof obj !== 'object' || obj instanceof Array) return; if (path.length === 0) throw new Error('Missing nested assignment path'); let first: string = path.shift(); if (typeof obj[first] === 'undefined') return; if (path.length > 1 && (typeof obj[first] !== 'object' || obj[first] instanceof Array)) return; if (path.length === 0) delete obj[first]; else Util.removeNestedValue(obj[first], path); } /** * Fetches a nested value from within an object via the * provided path * @static * @method getNestedValue * @param {any} obj Object to search * @param {string[]} path Nested path to follow within the object * @returns {any} */ public static getNestedValue(obj: any, path: string[]): any { if (typeof obj === 'undefined') return; if (path.length === 0) return obj; let first: string = path.shift(); if (typeof obj[first] === 'undefined') return; if (path.length > 1 && (typeof obj[first] !== 'object' || obj[first] instanceof Array)) return; return Util.getNestedValue(obj[first], path); } /** * Converts a TypeScript-style argument list into a valid args data object * for [resolve]{@link module:Middleware.resolve} and [expect]{@link module:Middleware.expect}. * This can help if the object syntax for resolving/expecting Command * arguments is too awkward or cluttered, or if a simpler syntax is * overall preferred. * * Args marked with `?` (for example: `arg?: String`) are declared as * optional and will be converted to `'[arg]': 'String'` at runtime. * Normal args will convert to `'': 'String'` * * Example: * ``` * `user: User, height: ['short', 'tall'], ...desc?: String` * // becomes: * { '': 'User', '': ['short', 'tall'], '[...desc]': 'String' } * ``` * * When specifying argument types for [resolve]{@link module:Middleware.resolve}, * use `String` when you know you will be later giving a string literal array to * [expect]{@link module:Middleware.expect} for the corresponding arg * @static * @method parseArgTypes * @param {string} input Argument list string * @returns {object} */ public static parseArgTypes(input: string): { [arg: string]: string | string[] } { let argStringRegex: RegExp = /(?:\.\.\.)?\w+\?? *: *(?:\[.*?\](?= *, *)|(?:\[.*?\] *$)|\w+)/g; if (!argStringRegex.test(input)) throw new Error(`Input string is incorrectly formatted: ${input}`); let output: { [arg: string]: string | string[] } = {}; let args: string[] = input.match(argStringRegex); for (let arg of args) { let split: string[] = arg.split(':').map(a => a.trim()); let name: string = split.shift(); arg = split.join(':'); if (/(?:\.\.\.)?.+\?/.test(name)) name = `[${name.replace('?', '')}]`; else name = `<${name}>`; if (/\[ *(?:(?: *, *)?(['"])(\S+)\1)+ *\]|\[ *\]/.test(arg)) { const data: string = arg.match(/\[(.*)\]/)[1]; if (!data) throw new Error('String literal array cannot be empty'); const values: string[] = data .split(',') .map(a => a.trim().slice(1, -1)); output[name] = values; } else output[name] = arg; } return output; } /** * Implementation of `performance-now` * @static * @method now * @returns {number} */ public static now(): number { type NSFunction = (hr?: [number, number]) => number; const ns: NSFunction = (hr = process.hrtime()) => hr[0] * 1e9 + hr[1]; return (ns() - (ns() - (process.uptime() * 1e9))) / 1e6; } }