1875 lines
54 KiB
JavaScript
1875 lines
54 KiB
JavaScript
|
registerPlugin({
|
||
|
name: "Command Library",
|
||
|
description: "Library to handle and manage commands",
|
||
|
version: "1.4.4",
|
||
|
author: "Multivitamin <david.kartnaller@gmail.com>",
|
||
|
autorun: true,
|
||
|
backends: ["ts3", "discord"],
|
||
|
vars: [{
|
||
|
name: "NOT_FOUND_MESSAGE",
|
||
|
title: "Send a message if no command has been found?",
|
||
|
type: "select",
|
||
|
options: ["YES", "NO"],
|
||
|
default: "1"
|
||
|
}, {
|
||
|
name: "DEBUGLEVEL",
|
||
|
title: "Debug Messages (default is INFO)",
|
||
|
type: "select",
|
||
|
options: ["ERROR", "WARNING", "INFO", "VERBOSE"],
|
||
|
default: "2"
|
||
|
}]
|
||
|
}, (_, { DEBUGLEVEL, NOT_FOUND_MESSAGE }, { version }) => {
|
||
|
|
||
|
const engine = require("engine")
|
||
|
const event = require("event")
|
||
|
const backend = require("backend")
|
||
|
const format = require("format")
|
||
|
|
||
|
/**
|
||
|
* @param {number} level
|
||
|
* @return {(mode: number) => (...args: any[]) => void}
|
||
|
* @private
|
||
|
*/
|
||
|
function DEBUG(level) {
|
||
|
/**
|
||
|
* @param {number} mode the loglevel to log
|
||
|
* @param {number[]} args data to log
|
||
|
* @private
|
||
|
*/
|
||
|
const logger = (mode, ...args) => {
|
||
|
if (mode > level) return
|
||
|
engine.log(...args)
|
||
|
}
|
||
|
|
||
|
return mode => logger.bind(null, mode)
|
||
|
}
|
||
|
DEBUG.VERBOSE = 3
|
||
|
DEBUG.INFO = 2
|
||
|
DEBUG.WARNING = 1
|
||
|
DEBUG.ERROR = 0
|
||
|
/** @private */
|
||
|
const debug = DEBUG(parseInt(DEBUGLEVEL, 10))
|
||
|
|
||
|
|
||
|
////////////////////////////////////////////////////////////
|
||
|
//// TYPES ////
|
||
|
////////////////////////////////////////////////////////////
|
||
|
|
||
|
/**
|
||
|
* callback for the command event
|
||
|
* @callback createArgumentHandler
|
||
|
* @param {ArgType} arg
|
||
|
* @returns {Argument}
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @typedef ArgType
|
||
|
* @type {object}
|
||
|
* @property {StringArgument} string
|
||
|
* @property {NumberArgument} number
|
||
|
* @property {ClientArgument} client
|
||
|
* @property {RestArgument} rest
|
||
|
* @property {GroupArgument} or
|
||
|
* @property {GroupArgument} and
|
||
|
*/
|
||
|
// eslint-disable-next-line no-unused-vars
|
||
|
const ArgType = {}
|
||
|
|
||
|
/**
|
||
|
* @ignore
|
||
|
* @typedef CommanderTextMessage
|
||
|
* @type {object}
|
||
|
* @property {(msg: string) => void} reply function to reply back
|
||
|
* @property {Client} client the client which invoked the command
|
||
|
* @property {Record<string, any>} arguments arguments from the command
|
||
|
* @property {Message} raw raw message
|
||
|
* @property {DiscordMessage} [message]
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @ignore
|
||
|
* @typedef MessageEvent
|
||
|
* @type {object}
|
||
|
* @property {Client} client
|
||
|
* @property {Channel} channel
|
||
|
* @property {string} text
|
||
|
* @property {number} mode
|
||
|
* @property {DiscordMessage} [message]
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* callback for the command event
|
||
|
* @callback execHandler
|
||
|
* @param {Client} invoker
|
||
|
* @param {Record<string, any>} args
|
||
|
* @param {(msg: string) => void} reply
|
||
|
* @param {MessageEvent} event
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* callback for the command event
|
||
|
* @callback permissionHandler
|
||
|
* @param {Client} invoker
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @ignore
|
||
|
* @typedef ThrottleInterface
|
||
|
* @property {number} points
|
||
|
* @property {number} next
|
||
|
* @property {number} timeout
|
||
|
*/
|
||
|
|
||
|
|
||
|
////////////////////////////////////////////////////////////
|
||
|
//// EXCEPTIONS ////
|
||
|
////////////////////////////////////////////////////////////
|
||
|
|
||
|
/**
|
||
|
* class representing a ThrottleError
|
||
|
* @private
|
||
|
*/
|
||
|
class ThrottleError extends Error {
|
||
|
/** @param {string} err */
|
||
|
constructor(err) {
|
||
|
super(err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* class representing a TooManyArguments
|
||
|
* @private
|
||
|
*/
|
||
|
class TooManyArgumentsError extends Error {
|
||
|
/**
|
||
|
* @param {string} err
|
||
|
* @param {ParseError|undefined} parseError
|
||
|
*/
|
||
|
constructor(err, parseError) {
|
||
|
super(err)
|
||
|
this.parseError = parseError
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* class representing a ParseError
|
||
|
* gets thrown when an Argument has not been parsed successful
|
||
|
* @private
|
||
|
*/
|
||
|
class ParseError extends Error {
|
||
|
/**
|
||
|
* @param {string} err
|
||
|
* @param {Argument} argument
|
||
|
*/
|
||
|
constructor(err, argument) {
|
||
|
super(err)
|
||
|
this.argument = argument
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* class representing a SubCommandNotFoundError
|
||
|
* @private
|
||
|
*/
|
||
|
class CommandNotFoundError extends Error {
|
||
|
/** @param {string} err */
|
||
|
constructor(err) {
|
||
|
super(err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* class representing a PermissionError
|
||
|
* @private
|
||
|
*/
|
||
|
class PermissionError extends Error {
|
||
|
/** @param {string} err */
|
||
|
constructor(err) {
|
||
|
super(err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
////////////////////////////////////////////////////////////
|
||
|
//// ARGUMENTS ////
|
||
|
////////////////////////////////////////////////////////////
|
||
|
|
||
|
/**
|
||
|
* @name Argument
|
||
|
*/
|
||
|
class Argument {
|
||
|
|
||
|
constructor() {
|
||
|
/**
|
||
|
* @type {boolean}
|
||
|
* @private
|
||
|
*/
|
||
|
this._optional = false
|
||
|
/**
|
||
|
* @type {string}
|
||
|
* @private
|
||
|
*/
|
||
|
this._name = "_"
|
||
|
/**
|
||
|
* @type {string}
|
||
|
* @private
|
||
|
*/
|
||
|
this._display = "_"
|
||
|
/**
|
||
|
* @type {boolean}
|
||
|
* @private
|
||
|
*/
|
||
|
this._displayDefault = true
|
||
|
/**
|
||
|
* @type {any}
|
||
|
* @private
|
||
|
*/
|
||
|
this._default = undefined
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @abstract
|
||
|
* @param {string} args
|
||
|
* @returns {any[]}
|
||
|
*/
|
||
|
validate(args) {
|
||
|
throw new Error("not implemented")
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets an Argument as optional
|
||
|
* if the argument has not been parsed successful it will use the first argument which has been given inside this method
|
||
|
* @param {any} [fallback] the default value which should be set if this parameter has not been found
|
||
|
* @param {boolean} [displayDefault] wether it should display the default value when called with the #getUsage method
|
||
|
*/
|
||
|
optional(fallback, displayDefault = true) {
|
||
|
this._displayDefault = displayDefault
|
||
|
this._default = fallback
|
||
|
this._optional = true
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/** retrieves the default value if it had been set */
|
||
|
getDefault() {
|
||
|
return this._default
|
||
|
}
|
||
|
|
||
|
/** checks if the Argument has a default value */
|
||
|
hasDefault() {
|
||
|
return this._default !== undefined
|
||
|
}
|
||
|
|
||
|
/** gets the manual of a command */
|
||
|
getManual() {
|
||
|
if (this.isOptional()) {
|
||
|
if (this._displayDefault && this.hasDefault()) {
|
||
|
return `[${this._display}=${this.getDefault()}]`
|
||
|
} else {
|
||
|
return `[${this._display}]`
|
||
|
}
|
||
|
} else {
|
||
|
return `<${this._display}>`
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** checks if the Argument is optional */
|
||
|
isOptional() {
|
||
|
return this._optional
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets a name for the argument to identify it later when the command gets dispatched
|
||
|
* This name will be used when passing the parsed argument to the exec function
|
||
|
* @param {string} name sets the name of the argument
|
||
|
* @param {string} [display] sets a beautified display name which will be used when the getManual command gets executed, if none given it will use the first parameter as display value
|
||
|
*/
|
||
|
setName(name, display) {
|
||
|
this._display = display === undefined ? name : display
|
||
|
if (typeof name !== "string") throw new Error("Argument of setName needs to be a string")
|
||
|
if (name.length < 1) throw new Error("Argument of setName needs to be at least 1 char long")
|
||
|
if (!name.match(/^[a-z0-9_]+$/i)) throw new Error("Argument of setName should contain only chars A-z, 0-9 and _")
|
||
|
this._name = name
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieves the name of the Argument
|
||
|
* @returns {string} retrieves the arguments name
|
||
|
*/
|
||
|
getName() {
|
||
|
return this._name
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* creates new object with argument options
|
||
|
* @returns {ArgType}
|
||
|
*/
|
||
|
static createArgumentLayer() {
|
||
|
return {
|
||
|
string: new StringArgument(),
|
||
|
number: new NumberArgument(),
|
||
|
client: new ClientArgument(),
|
||
|
rest: new RestArgument(),
|
||
|
or: new GroupArgument("or"),
|
||
|
and: new GroupArgument("and")
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @name StringArgument
|
||
|
*/
|
||
|
class StringArgument extends Argument {
|
||
|
|
||
|
constructor() {
|
||
|
super()
|
||
|
/**
|
||
|
* @type {?RegExp}
|
||
|
* @private
|
||
|
*/
|
||
|
this._regex = null
|
||
|
/**
|
||
|
* @type {?number}
|
||
|
* @private
|
||
|
*/
|
||
|
this._maxlen = null
|
||
|
/**
|
||
|
* @type {?number}
|
||
|
* @private
|
||
|
*/
|
||
|
this._minlen = null
|
||
|
/**
|
||
|
* @type {?string[]}
|
||
|
* @private
|
||
|
*/
|
||
|
this._whitelist = null
|
||
|
/**
|
||
|
* @type {boolean}
|
||
|
* @private
|
||
|
*/
|
||
|
this._uppercase = false
|
||
|
/**
|
||
|
* @type {boolean}
|
||
|
* @private
|
||
|
*/
|
||
|
this._lowercase = false
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Validates the given String to the StringArgument
|
||
|
* @param {string} args the remaining args
|
||
|
*/
|
||
|
validate(args) {
|
||
|
const argArray = args.split(" ")
|
||
|
const str = argArray.shift()
|
||
|
return this._validate(str||"", argArray.join(" "))
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Validates the given string to the StringArgument params
|
||
|
* @protected
|
||
|
* @param {string} arg string argument that should be parsed
|
||
|
* @param {string[]} rest the remaining args
|
||
|
*/
|
||
|
_validate(arg, ...rest) {
|
||
|
if (this._uppercase) arg = arg.toUpperCase()
|
||
|
if (this._lowercase) arg = arg.toLowerCase()
|
||
|
if (this._minlen !== null && this._minlen > arg.length) throw new ParseError(`String length not greater or equal! Expected at least ${this._minlen}, but got ${arg.length}`, this)
|
||
|
if (this._maxlen !== null && this._maxlen < arg.length) throw new ParseError(`String length not less or equal! Maximum ${this._maxlen} chars allowed, but got ${arg.length}`, this)
|
||
|
if (this._whitelist !== null && !this._whitelist.includes(arg)) throw new ParseError(`Invalid Input for ${arg}. Allowed words: ${this._whitelist.join(", ")}`, this)
|
||
|
if (this._regex !== null && !this._regex.test(arg)) throw new ParseError(`Regex missmatch, the input '${arg}' did not match the expression ${this._regex.toString()}`, this)
|
||
|
return [arg, ...rest]
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Matches a regular expression pattern
|
||
|
* @param {RegExp} regex the regex which should be validated
|
||
|
*/
|
||
|
match(regex) {
|
||
|
this._regex = regex
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the maximum Length of the String
|
||
|
* @param {number} len the maximum length of the argument
|
||
|
*/
|
||
|
max(len) {
|
||
|
this._maxlen = len
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the minimum Length of the String
|
||
|
* @param {number} len the minimum length of the argument
|
||
|
*/
|
||
|
min(len) {
|
||
|
this._minlen = len
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
|
||
|
/** converts the input to an upper case string */
|
||
|
forceUpperCase() {
|
||
|
this._lowercase = false
|
||
|
this._uppercase = true
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
|
||
|
/** converts the input to a lower case string */
|
||
|
forceLowerCase() {
|
||
|
this._lowercase = true
|
||
|
this._uppercase = false
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* creates a list of available whitelisted words
|
||
|
* @param {string[]} words array of whitelisted words
|
||
|
*/
|
||
|
whitelist(words) {
|
||
|
if (!Array.isArray(this._whitelist)) this._whitelist = []
|
||
|
this._whitelist.push(...words)
|
||
|
return this
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @name RestArgument
|
||
|
*/
|
||
|
class RestArgument extends StringArgument {
|
||
|
|
||
|
/**
|
||
|
* Validates the given String to the RestArgument
|
||
|
* @param {string} args the remaining args
|
||
|
*/
|
||
|
validate(args) {
|
||
|
return super._validate(args, "")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @name NumberArgument
|
||
|
*/
|
||
|
class NumberArgument extends Argument {
|
||
|
|
||
|
constructor() {
|
||
|
super()
|
||
|
/**
|
||
|
* @type {?number}
|
||
|
* @private
|
||
|
*/
|
||
|
this._min = null
|
||
|
/**
|
||
|
* @type {?number}
|
||
|
* @private
|
||
|
*/
|
||
|
this._max = null
|
||
|
/**
|
||
|
* @type {boolean}
|
||
|
* @private
|
||
|
*/
|
||
|
this._int = false
|
||
|
/**
|
||
|
* @type {boolean}
|
||
|
* @private
|
||
|
*/
|
||
|
this._forcePositive = false
|
||
|
/**
|
||
|
* @type {boolean}
|
||
|
* @private
|
||
|
*/
|
||
|
this._forceNegative = false
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Validates the given Number to the Object
|
||
|
* @param {string} args the remaining args
|
||
|
*/
|
||
|
validate(args) {
|
||
|
const argArray = args.split(" ")
|
||
|
const arg = argArray.shift()|| ""
|
||
|
const num = parseFloat(arg)
|
||
|
if (!(/^-?\d+(\.\d+)?$/).test(arg) || isNaN(num)) throw new ParseError(`"${arg}" is not a valid number`, this)
|
||
|
if (this._min !== null && this._min > num) throw new ParseError(`Number not greater or equal! Expected at least ${this._min}, but got ${num}`, this)
|
||
|
if (this._max !== null && this._max < num) throw new ParseError(`Number not less or equal! Expected at least ${this._max}, but got ${num}`, this)
|
||
|
if (this._int && num % 1 !== 0) throw new ParseError(`Given Number is not an Integer! (${num})`, this)
|
||
|
if (this._forcePositive && num <= 0) throw new ParseError(`Given Number is not Positive! (${num})`, this)
|
||
|
if (this._forceNegative && num >= 0) throw new ParseError(`Given Number is not Negative! (${num})`, this)
|
||
|
return [num, argArray.join(" ")]
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* specifies the minimum value
|
||
|
* @param {number} min the minimum length of the argument
|
||
|
*/
|
||
|
min(min) {
|
||
|
this._min = min
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* specifies the maximum value
|
||
|
* @param {number} max the maximum length of the argument
|
||
|
*/
|
||
|
max(max) {
|
||
|
this._max = max
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/** specifies that the Number must be an integer (no floating point) */
|
||
|
integer() {
|
||
|
this._int = true
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/** specifies that the Number must be a positive Number */
|
||
|
positive() {
|
||
|
this._forcePositive = true
|
||
|
this._forceNegative = false
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/** specifies that the Number must be a negative Number */
|
||
|
negative() {
|
||
|
this._forcePositive = false
|
||
|
this._forceNegative = true
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Class representing a ClientArgument
|
||
|
* this Argument is capable to parse a Client UID or a simple UID
|
||
|
* inside the exec function it will resolve the found uid
|
||
|
* @name ClientArgument
|
||
|
*/
|
||
|
class ClientArgument extends Argument {
|
||
|
|
||
|
/**
|
||
|
* Validates and tries to parse the Client from the given input string
|
||
|
* @param {string} args the input from where the client gets extracted
|
||
|
*/
|
||
|
validate(args) {
|
||
|
switch (engine.getBackend()) {
|
||
|
case "ts3": return this._validateTS3(args)
|
||
|
case "discord": return this._validateDiscord(args)
|
||
|
default: throw new Error(`Unknown Backend ${engine.getBackend()}`)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Tries to validate a TeamSpeak Client URL or UID
|
||
|
* @param {string} args the input from where the client gets extracted
|
||
|
* @private
|
||
|
*/
|
||
|
_validateTS3(args) {
|
||
|
const match = args.match(/^(\[URL=client:\/\/\d*\/(?<url_uid>[/+a-z0-9]{27}=)~.*\].*\[\/URL\]|(?<uid>[/+a-z0-9]{27}=)) *(?<rest>.*)$/i)
|
||
|
if (!match || !match.groups) throw new ParseError("Client not found!", this)
|
||
|
return [match.groups.url_uid || match.groups.uid, match.groups.rest]
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Tries to validate a Discord Client Name or ID
|
||
|
* @param {string} args the input from where the client gets extracted
|
||
|
* @private
|
||
|
*/
|
||
|
_validateDiscord(args) {
|
||
|
const match = args.match(/^(<@(?<id>\d{18})>|@(?<name>.*?)#\d{4}) *(?<rest>.*)$/i)
|
||
|
if (!match || !match.groups) throw new ParseError("Client not found!", this)
|
||
|
const { id, name, rest } = match.groups
|
||
|
if (id) {
|
||
|
return [id, rest]
|
||
|
} else if (name) {
|
||
|
const client = backend.getClientByName(name)
|
||
|
if (!client) throw new ParseError("Client not found!", this)
|
||
|
return [client.uid().split("/")[1], rest]
|
||
|
} else {
|
||
|
throw new ParseError("Client not found!", this)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @name GroupArgument
|
||
|
*/
|
||
|
class GroupArgument extends Argument {
|
||
|
|
||
|
/**
|
||
|
* @param {"or"|"and"} type
|
||
|
*/
|
||
|
constructor(type) {
|
||
|
super()
|
||
|
/**
|
||
|
* @type {"or"|"and"}
|
||
|
* @private
|
||
|
*/
|
||
|
this._type = type
|
||
|
/**
|
||
|
* @type {Argument[]}
|
||
|
* @private
|
||
|
*/
|
||
|
this._arguments = []
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Validates the given String to the GroupArgument
|
||
|
* @param {string} args the remaining args
|
||
|
*/
|
||
|
validate(args) {
|
||
|
switch (this._type) {
|
||
|
case "or": return this._validateOr(args)
|
||
|
case "and": return this._validateAnd(args)
|
||
|
default: throw new Error(`got invalid group type '${this._type}'`)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Validates the given string to the "or" of the GroupArgument
|
||
|
* @param {string} args the remaining args
|
||
|
* @private
|
||
|
*/
|
||
|
_validateOr(args) {
|
||
|
/**
|
||
|
* @type {Error[]}
|
||
|
* @private
|
||
|
*/
|
||
|
const errors = []
|
||
|
/**
|
||
|
* @type {Record<string, any>}
|
||
|
* @private
|
||
|
*/
|
||
|
const resolved = {}
|
||
|
const valid = this._arguments.some(arg => {
|
||
|
try {
|
||
|
const result = arg.validate(args)
|
||
|
resolved[arg.getName()] = result[0]
|
||
|
return (args = result[1].trim(), true)
|
||
|
} catch (e) {
|
||
|
errors.push(e)
|
||
|
return false
|
||
|
}
|
||
|
})
|
||
|
if (!valid) throw new ParseError(`No valid match found`, this)
|
||
|
return [resolved, args]
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Validates the given string to the "and" of the GroupArgument
|
||
|
* @param {string} args the remaining args
|
||
|
* @private
|
||
|
*/
|
||
|
_validateAnd(args) {
|
||
|
/**
|
||
|
* @type {Record<string, any>}
|
||
|
* @private
|
||
|
*/
|
||
|
const resolved = {}
|
||
|
/**
|
||
|
* @type {?Error}
|
||
|
* @private
|
||
|
*/
|
||
|
let error = null
|
||
|
this._arguments.some(arg => {
|
||
|
try {
|
||
|
const result = arg.validate(args)
|
||
|
resolved[arg.getName()] = result[0]
|
||
|
return (args = result[1].trim(), false)
|
||
|
} catch (e) {
|
||
|
error = e
|
||
|
return true
|
||
|
}
|
||
|
})
|
||
|
if (error !== null) throw error
|
||
|
return [resolved, args]
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* adds an argument to the command
|
||
|
* @param {createArgumentHandler|Argument} arg an argument to add
|
||
|
*/
|
||
|
addArgument(arg) {
|
||
|
if (typeof arg === "function") arg = arg(Argument.createArgumentLayer())
|
||
|
if (!(arg instanceof Argument)) throw new Error(`Typeof arg should be function or instance of Argument but got ${arg}`)
|
||
|
this._arguments.push(arg)
|
||
|
return this
|
||
|
}
|
||
|
}
|
||
|
|
||
|
////////////////////////////////////////////////////////////
|
||
|
//// Throttle ////
|
||
|
////////////////////////////////////////////////////////////
|
||
|
|
||
|
/**
|
||
|
* @name Throttle
|
||
|
*/
|
||
|
class Throttle {
|
||
|
|
||
|
constructor() {
|
||
|
/**
|
||
|
* @type {Record<string, ThrottleInterface>}
|
||
|
* @private
|
||
|
*/
|
||
|
this._throttled = {}
|
||
|
/**
|
||
|
* @type {number}
|
||
|
* @private
|
||
|
*/
|
||
|
this._penalty = 1
|
||
|
/**
|
||
|
* @type {number}
|
||
|
* @private
|
||
|
*/
|
||
|
this._initial = 1
|
||
|
/**
|
||
|
* @type {number}
|
||
|
* @private
|
||
|
*/
|
||
|
this._restore = 1
|
||
|
/**
|
||
|
* @type {number}
|
||
|
* @private
|
||
|
*/
|
||
|
this._tickrate = 1000
|
||
|
}
|
||
|
|
||
|
/* clears all timers */
|
||
|
stop() {
|
||
|
Object.values(this._throttled).forEach(({ timeout }) => clearTimeout(timeout))
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Defines how fast points will get restored
|
||
|
* @param {number} duration time in ms how fast points should get restored
|
||
|
*/
|
||
|
tickRate(duration) {
|
||
|
this._tickrate = duration
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* The amount of points a command request costs
|
||
|
* @param {number} amount the amount of points that should be reduduced
|
||
|
*/
|
||
|
penaltyPerCommand(amount) {
|
||
|
this._penalty = amount
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* The Amount of Points that should get restored per tick
|
||
|
* @param {number} amount the amount that should get restored
|
||
|
*/
|
||
|
restorePerTick(amount) {
|
||
|
this._restore = amount
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the initial Points a user has at beginning
|
||
|
* @param {number} initial the Initial amount of Points a user has
|
||
|
*/
|
||
|
initialPoints(initial) {
|
||
|
this._initial = initial
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Reduces the given points for a Command for the given Client
|
||
|
* @param {Client} client the client which points should be removed
|
||
|
*/
|
||
|
throttle(client) {
|
||
|
this._reducePoints(client.uid())
|
||
|
return this.isThrottled(client)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Restores points from the given id
|
||
|
* @param {string} id the identifier for which the points should be stored
|
||
|
* @private
|
||
|
*/
|
||
|
_restorePoints(id) {
|
||
|
const throttle = this._throttled[id]
|
||
|
if (throttle === undefined) return
|
||
|
throttle.points += this._restore
|
||
|
if (throttle.points >= this._initial) {
|
||
|
Reflect.deleteProperty(this._throttled, id)
|
||
|
} else {
|
||
|
this._refreshTimeout(id)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Resets the timeout counter for a stored id
|
||
|
* @param {string} id the identifier which should be added
|
||
|
* @private
|
||
|
*/
|
||
|
_refreshTimeout(id) {
|
||
|
if (this._throttled[id] === undefined) return
|
||
|
clearTimeout(this._throttled[id].timeout)
|
||
|
// @ts-ignore
|
||
|
this._throttled[id].timeout = setTimeout(this._restorePoints.bind(this, id), this._tickrate)
|
||
|
this._throttled[id].next = Date.now() + this._tickrate
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Removes points from an id
|
||
|
* @param {string} id the identifier which should be added
|
||
|
* @private
|
||
|
*/
|
||
|
_reducePoints(id) {
|
||
|
const throttle = this._createIdIfNotExists(id)
|
||
|
throttle.points -= this._penalty
|
||
|
this._refreshTimeout(id)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* creates the identifier in the throttled object
|
||
|
* @param {string} id the identifier which should be added
|
||
|
* @private
|
||
|
*/
|
||
|
_createIdIfNotExists(id) {
|
||
|
if (Object.keys(this._throttled).includes(id)) return this._throttled[id]
|
||
|
this._throttled[id] = { points: this._initial, next: 0, timeout: 0 }
|
||
|
return this._throttled[id]
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Checks if the given Client is affected by throttle limitations
|
||
|
* @param {Client} client the TeamSpeak Client which should get checked
|
||
|
*/
|
||
|
isThrottled(client) {
|
||
|
const throttle = this._throttled[client.uid()]
|
||
|
if (throttle === undefined) return false
|
||
|
return throttle.points <= 0
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* retrieves the time in milliseconds until a client can send his next command
|
||
|
* @param {Client} client the client which should be checked
|
||
|
* @returns returns the time a client is throttled in ms
|
||
|
*/
|
||
|
timeTillNextCommand(client) {
|
||
|
if (this._throttled[client.uid()] === undefined) return 0
|
||
|
return this._throttled[client.uid()].next - Date.now()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
////////////////////////////////////////////////////////////
|
||
|
//// COMMAND ////
|
||
|
////////////////////////////////////////////////////////////
|
||
|
|
||
|
/**
|
||
|
* @name BaseCommand
|
||
|
*/
|
||
|
class BaseCommand {
|
||
|
|
||
|
/**
|
||
|
* @param {string} cmd
|
||
|
* @param {Collector} collector
|
||
|
*/
|
||
|
constructor(cmd, collector) {
|
||
|
/**
|
||
|
* @type {Collector}
|
||
|
* @protected
|
||
|
*/
|
||
|
this._collector = collector
|
||
|
/**
|
||
|
* @type {permissionHandler[]}
|
||
|
* @private
|
||
|
*/
|
||
|
this._permissionHandler = []
|
||
|
/**
|
||
|
* @type {execHandler[]}
|
||
|
* @protected
|
||
|
*/
|
||
|
this._execHandler = []
|
||
|
/**
|
||
|
* @type {string}
|
||
|
* @private
|
||
|
*/
|
||
|
this._prefix = ""
|
||
|
/**
|
||
|
* @type {string}
|
||
|
* @private
|
||
|
*/
|
||
|
this._help = ""
|
||
|
/**
|
||
|
* @type {string[]}
|
||
|
* @private
|
||
|
*/
|
||
|
this._manual = []
|
||
|
/**
|
||
|
* @type {string}
|
||
|
* @private
|
||
|
*/
|
||
|
this._name = cmd
|
||
|
/**
|
||
|
* @type {boolean}
|
||
|
* @private
|
||
|
*/
|
||
|
this._enabled = true
|
||
|
/**
|
||
|
* @type {?Throttle}
|
||
|
* @private
|
||
|
*/
|
||
|
this._throttle = null
|
||
|
/**
|
||
|
* @type {string[]}
|
||
|
* @private
|
||
|
*/
|
||
|
this._alias = []
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @abstract
|
||
|
* @returns {string}
|
||
|
*/
|
||
|
getUsage() {
|
||
|
throw new Error("not implemented")
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @abstract
|
||
|
* @param {Client} client
|
||
|
* @returns {Promise<boolean>}
|
||
|
*/
|
||
|
hasPermission(client) {
|
||
|
throw new Error("not implemented")
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @abstract
|
||
|
* @param {string} args
|
||
|
* @returns {Record<string, any>}
|
||
|
*/
|
||
|
validate(args) {
|
||
|
throw new Error("not implemented")
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @abstract
|
||
|
* @param {string} args
|
||
|
* @param {MessageEvent} ev
|
||
|
*/
|
||
|
dispatch(args, ev) {
|
||
|
throw new Error("not implemented")
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* one or more alias for this command
|
||
|
* @param {...string} alias
|
||
|
*/
|
||
|
alias(...alias) {
|
||
|
alias = alias.map(a => a.toLowerCase())
|
||
|
alias.forEach(a => Collector.isValidCommandName(a))
|
||
|
this._alias.push(...alias.filter(a => this._collector.getAvailableCommands(a)))
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/** checks if the command is enabled */
|
||
|
isEnabled() {
|
||
|
return this._enabled
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* enables the current command
|
||
|
*/
|
||
|
enable() {
|
||
|
this._enabled = true
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* disables the current command
|
||
|
*/
|
||
|
disable() {
|
||
|
this._enabled = false
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/** gets the command name without its prefix */
|
||
|
getCommandName() {
|
||
|
return this._name
|
||
|
}
|
||
|
|
||
|
/** retrieves all registered alias names without prefix */
|
||
|
getAlias() {
|
||
|
return this._alias
|
||
|
}
|
||
|
|
||
|
/** gets the command name with its prefix */
|
||
|
getFullCommandName() {
|
||
|
return `${this.getPrefix()}${this.getCommandName()}`
|
||
|
}
|
||
|
|
||
|
/** retrieves all registered alias names with prefix */
|
||
|
getFullAlias() {
|
||
|
return this._alias.map(a => `${this.getPrefix()}${a}`)
|
||
|
}
|
||
|
|
||
|
/** retrieves all registered command names */
|
||
|
getCommandNames() {
|
||
|
return [this.getCommandName(), ...this.getAlias()]
|
||
|
}
|
||
|
|
||
|
/** retrieves all registered command names with prefix */
|
||
|
getFullCommandNames() {
|
||
|
return [this.getFullCommandName(), ...this.getFullAlias()]
|
||
|
}
|
||
|
|
||
|
/** retrieves the help text */
|
||
|
getHelp() {
|
||
|
return this._help
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* sets a help text (should be a very brief description)
|
||
|
* @param {string} text help text
|
||
|
*/
|
||
|
help(text) {
|
||
|
this._help = text
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/** returns a boolean wether a help text has been set or not */
|
||
|
hasHelp() {
|
||
|
return this._help !== ""
|
||
|
}
|
||
|
|
||
|
/** retrieves the current manual text */
|
||
|
getManual() {
|
||
|
return this._manual.join("\r\n")
|
||
|
}
|
||
|
|
||
|
/** returns a boolean wether a help text has been set or not */
|
||
|
hasManual() {
|
||
|
return this._manual.length > 0
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {string} prefix the new prefix to set
|
||
|
*/
|
||
|
forcePrefix(prefix) {
|
||
|
this._prefix = prefix
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/** gets the current prefix for this command */
|
||
|
getPrefix() {
|
||
|
if (this._prefix.length > 0) return this._prefix
|
||
|
return Collector.getCommandPrefix()
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* sets a manual text, this function can be called multiple times
|
||
|
* in order to create a multilined manual text
|
||
|
* @param {string} text the manual text
|
||
|
*/
|
||
|
manual(text) {
|
||
|
this._manual.push(text)
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* clears the current manual text
|
||
|
*/
|
||
|
clearManual() {
|
||
|
this._manual = []
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* register an execution handler for this command
|
||
|
* @param {execHandler} callback gets called whenever the command should do something
|
||
|
*/
|
||
|
exec(callback) {
|
||
|
this._execHandler.push(callback)
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* adds an instance of a throttle class
|
||
|
* @param {Throttle} throttle adds the throttle instance
|
||
|
*/
|
||
|
addThrottle(throttle) {
|
||
|
this._throttle = throttle
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {Client} client the sinusbot client
|
||
|
* @private
|
||
|
*/
|
||
|
_handleThrottle(client) {
|
||
|
if (!(this._throttle instanceof Throttle)) return
|
||
|
if (this._throttle.isThrottled(client)) {
|
||
|
const time = (this._throttle.timeTillNextCommand(client) / 1000).toFixed(1)
|
||
|
throw new ThrottleError(`You can use this command again in ${time} seconds!`)
|
||
|
} else {
|
||
|
this._throttle.throttle(client)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* register a permission handler for this command
|
||
|
* @param {permissionHandler} callback gets called whenever the permission for a client gets checked
|
||
|
*/
|
||
|
checkPermission(callback) {
|
||
|
this._permissionHandler.push(callback)
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* checks if a client is allowed to use this command
|
||
|
* this is the low level method to check permissions for a single command
|
||
|
* @param {Client} client sinusbot client to check permissions from
|
||
|
*/
|
||
|
isAllowed(client) {
|
||
|
return Promise.all(this._permissionHandler.map(cb => cb(client)))
|
||
|
.then(res => res.every(r => r))
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* dispatches a command
|
||
|
* @protected
|
||
|
* @param {CommanderTextMessage} ev
|
||
|
*/
|
||
|
async _dispatchCommand(ev) {
|
||
|
if (!(await this.hasPermission(ev.client)))
|
||
|
throw new PermissionError("no permission to execute this command")
|
||
|
this._handleThrottle(ev.client)
|
||
|
this._execHandler.forEach(handle => handle(ev.client, ev.arguments, ev.reply, ev.raw))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @name Command
|
||
|
*/
|
||
|
class Command extends BaseCommand {
|
||
|
|
||
|
/**
|
||
|
* @param {string} cmd
|
||
|
* @param {Collector} collector
|
||
|
*/
|
||
|
constructor(cmd, collector) {
|
||
|
super(cmd, collector)
|
||
|
/**
|
||
|
* @type {Argument[]}
|
||
|
* @private
|
||
|
*/
|
||
|
this._arguments = []
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieves the usage of the command with its parameterized names
|
||
|
* @returns retrieves the complete usage of the command with its argument names
|
||
|
*/
|
||
|
getUsage() {
|
||
|
return `${this.getCommandName()} ${this.getArguments().map(arg => arg.getManual()).join(" ")}`
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* checks if a client should have permission to use this command
|
||
|
* @param {Client} client the client which should be checked
|
||
|
*/
|
||
|
hasPermission(client) {
|
||
|
return this.isAllowed(client)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* adds an argument to the command
|
||
|
* @param {createArgumentHandler|Argument} arg an argument to add
|
||
|
*/
|
||
|
addArgument(arg) {
|
||
|
if (typeof arg === "function") arg = arg(Argument.createArgumentLayer())
|
||
|
if (!(arg instanceof Argument)) throw new Error(`Typeof arg should be function or instance of Argument but got ${arg}`)
|
||
|
this._arguments.push(arg)
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/** retrieves all available arguments */
|
||
|
getArguments() {
|
||
|
return this._arguments
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Validates the command
|
||
|
* @param {string} args the arguments from the command which should be validated
|
||
|
*/
|
||
|
validate(args) {
|
||
|
const { result, errors, remaining } = this.validateArgs(args)
|
||
|
if (remaining.length > 0) throw new TooManyArgumentsError(`Too many argument!`, errors.shift())
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {string} args
|
||
|
* @param {MessageEvent} ev
|
||
|
*/
|
||
|
dispatch(args, ev) {
|
||
|
return this._dispatchCommand({
|
||
|
...ev,
|
||
|
arguments: this.validate(args),
|
||
|
reply: Collector.getReplyOutput(ev),
|
||
|
raw: ev
|
||
|
})
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Validates the given input string to all added arguments
|
||
|
* @param {string} args the string which should get validated
|
||
|
*/
|
||
|
validateArgs(args) {
|
||
|
args = args.trim()
|
||
|
/**
|
||
|
* @type {Record<string, any>}
|
||
|
* @private
|
||
|
*/
|
||
|
const result = {}
|
||
|
/**
|
||
|
* @type {ParseError[]}
|
||
|
* @private
|
||
|
*/
|
||
|
const errors = []
|
||
|
this.getArguments().forEach(arg => {
|
||
|
try {
|
||
|
const [val, rest] = arg.validate(args)
|
||
|
result[arg.getName()] = val
|
||
|
return args = rest.trim()
|
||
|
} catch (e) {
|
||
|
if (e instanceof ParseError && arg.isOptional()) {
|
||
|
result[arg.getName()] = arg.getDefault()
|
||
|
return errors.push(e)
|
||
|
}
|
||
|
throw e
|
||
|
}
|
||
|
})
|
||
|
return { result, remaining: args, errors }
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @name CommandGroup
|
||
|
*/
|
||
|
class CommandGroup extends BaseCommand {
|
||
|
|
||
|
/**
|
||
|
* @param {string} cmd
|
||
|
* @param {Collector} collector
|
||
|
*/
|
||
|
constructor(cmd, collector) {
|
||
|
super(cmd, collector)
|
||
|
/**
|
||
|
* @type {Command[]}
|
||
|
* @private
|
||
|
*/
|
||
|
this._commands = []
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieves the usage of the command with its parameterized names
|
||
|
* @returns retrieves the complete usage of the command with its argument names
|
||
|
*/
|
||
|
getUsage() {
|
||
|
return `${this.getFullCommandName()} ${this._commands.map(cmd => cmd.getCommandName()).join("|")}`
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* checks if a client should have permission to use this command
|
||
|
* @param {Client} client the client which should be checked
|
||
|
*/
|
||
|
async hasPermission(client) {
|
||
|
if (!await this.isAllowed(client)) return false
|
||
|
if (this._execHandler.length > 0) return true
|
||
|
return (await Promise.all(this._commands.map(cmd => cmd.hasPermission(client)))).some(result => result)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Adds a new sub Commmand to the group
|
||
|
* @param {string} name the sub command name which should be added
|
||
|
*/
|
||
|
addCommand(name) {
|
||
|
name = name.toLowerCase()
|
||
|
if (!Collector.isValidCommandName(name)) throw new Error("Can not create a command with length of 0")
|
||
|
const cmd = new Command(name, this._collector)
|
||
|
this._commands.push(cmd)
|
||
|
return cmd
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieves a subcommand by its command name
|
||
|
* @param {string} name the name which should be searched for
|
||
|
*/
|
||
|
findCommandByName(name) {
|
||
|
name = name.toLowerCase()
|
||
|
if (name.length === 0) throw new CommandNotFoundError(`No subcommand specified for Command ${this.getFullCommandName()}`)
|
||
|
const cmd = this._commands.find(c => c.getCommandNames().includes(name))
|
||
|
if (!cmd) throw new CommandNotFoundError(`Command with name "${name}" has not been found on Command ${this.getFullCommandName()}!`)
|
||
|
return cmd
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* retrievel all available subcommands
|
||
|
* @param {Client} [client] the sinusbot client for which the commands should be retrieved if none has been omitted it will retrieve all available commands
|
||
|
* @param {string} [cmd] the command which should be searched for
|
||
|
*/
|
||
|
getAvailableCommands(client, cmd) {
|
||
|
const cmds = this._commands
|
||
|
.filter(c => c.getCommandName() === cmd || !cmd)
|
||
|
.filter(c => c.isEnabled())
|
||
|
if (!client) return Promise.resolve(cmds)
|
||
|
return Collector.checkPermissions(cmds, client)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {string} args
|
||
|
* @param {MessageEvent} ev
|
||
|
*/
|
||
|
async dispatch(args, ev) {
|
||
|
const [cmd, ...rest] = args.split(" ")
|
||
|
if (!await this.hasPermission(ev.client))
|
||
|
throw new PermissionError("not enough permission to execute this command")
|
||
|
if (cmd.length === 0) {
|
||
|
return this._dispatchCommand({
|
||
|
...ev,
|
||
|
arguments: {},
|
||
|
reply: Collector.getReplyOutput(ev),
|
||
|
raw: ev
|
||
|
})
|
||
|
}
|
||
|
return this.findCommandByName(cmd).dispatch(rest.join(" "), ev)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
////////////////////////////////////////////////////////////
|
||
|
//// Collector ////
|
||
|
////////////////////////////////////////////////////////////
|
||
|
|
||
|
/**
|
||
|
* @name Collector
|
||
|
*/
|
||
|
class Collector {
|
||
|
|
||
|
constructor() {
|
||
|
/**
|
||
|
* @type {BaseCommand[]}
|
||
|
* @private
|
||
|
*/
|
||
|
this._commands = []
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* retrieves the current Command Prefix
|
||
|
* @returns {string} returns the command prefix
|
||
|
*/
|
||
|
static getCommandPrefix() {
|
||
|
const prefix = engine.getCommandPrefix()
|
||
|
if (typeof prefix !== "string" || prefix.length === 0) return "!"
|
||
|
return prefix
|
||
|
}
|
||
|
|
||
|
/** creates a new Throttle instance */
|
||
|
static createThrottle() {
|
||
|
return new Throttle()
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* retrieves the correct reply chat from where the client has sent the message
|
||
|
* @param {Message} event
|
||
|
* @returns {(msg: string) => void}
|
||
|
*/
|
||
|
static getReplyOutput({ mode, client, channel }) {
|
||
|
switch (mode) {
|
||
|
case 1: return client.chat.bind(client)
|
||
|
case 2: return channel.chat.bind(channel)
|
||
|
case 3: return backend.chat.bind(backend)
|
||
|
default: return msg => debug(DEBUG.WARNING)(`WARN no reply channel set for mode ${mode}, message "${msg}" not sent!`)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* checks the permissions from a set of commands
|
||
|
* @param {BaseCommand[]} commands
|
||
|
* @param {Client} client
|
||
|
* @returns {Promise<BaseCommand[]>}
|
||
|
*/
|
||
|
static async checkPermissions(commands, client) {
|
||
|
const result = await Promise.all(commands.map(cmd => cmd.hasPermission(client)))
|
||
|
return commands.filter((_, i) => result[i])
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* checks if the command name is valid
|
||
|
* @param {string} name
|
||
|
*/
|
||
|
static isValidCommandName(name) {
|
||
|
if (typeof name !== "string") throw new Error("Expected a string as command name!")
|
||
|
if (name.length < 1) throw new Error(`Command should have a minimum length of 1!`)
|
||
|
if ((/\s/).test(name)) throw new Error(`Command "${name}" should not contain spaces!`)
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* get all available commands from its command string
|
||
|
* @param {string} name
|
||
|
*/
|
||
|
getAvailableCommands(name) {
|
||
|
name = name.toLowerCase()
|
||
|
return this._commands
|
||
|
.filter(cmd => cmd.isEnabled())
|
||
|
.filter(cmd => cmd.getCommandNames().includes(name))
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* retrieves all available permissions for a certain client
|
||
|
* @param {Client} client
|
||
|
*/
|
||
|
getAvailableCommandsByPermission(client) {
|
||
|
return Collector.checkPermissions(
|
||
|
this._commands.filter(cmd => cmd.isEnabled()),
|
||
|
client
|
||
|
)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Searches for one or multiple enabled commands with its prefix
|
||
|
* @param {string} name the command with its prefix
|
||
|
* @returns {BaseCommand[]} returns an array of found commands
|
||
|
*/
|
||
|
getAvailableCommandsWithPrefix(name) {
|
||
|
name = name.toLowerCase()
|
||
|
return this._commands
|
||
|
.filter(cmd => cmd.isEnabled())
|
||
|
.filter(cmd => cmd.getFullCommandNames().includes(name))
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* checks if a command is a possible command string
|
||
|
* @param {string} text
|
||
|
*/
|
||
|
isPossibleCommand(text) {
|
||
|
if (text.startsWith(Collector.getCommandPrefix())) return true
|
||
|
return this._commands.some(cmd => cmd.getFullCommandNames().includes(text.split(" ")[0]))
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* creates a new command
|
||
|
* @param {string} name the name of the command
|
||
|
*/
|
||
|
registerCommand(name) {
|
||
|
name = name.toLowerCase()
|
||
|
if (!Collector.isValidCommandName(name))
|
||
|
throw new Error("Can not create a command with length of 0")
|
||
|
const cmd = new Command(name, this)
|
||
|
this._commands.push(cmd)
|
||
|
return cmd
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* creates a new command
|
||
|
* @param {string} name the name of the command
|
||
|
*/
|
||
|
registerCommandGroup(name) {
|
||
|
name = name.toLowerCase()
|
||
|
if (!Collector.isValidCommandName(name))
|
||
|
throw new Error("Can not create a command with length of 0")
|
||
|
const cmd = new CommandGroup(name, this)
|
||
|
this._commands.push(cmd)
|
||
|
return cmd
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* checks if the command string is save to register as a new command
|
||
|
* this function basically checks if there is no other command named with
|
||
|
* throws an error when Collector#validateCommandName errors
|
||
|
* returns false when this command has been already registered
|
||
|
* returns true when this is a completely unused command
|
||
|
* @param {string} cmd
|
||
|
*/
|
||
|
isSaveCommand(cmd) {
|
||
|
cmd = cmd.toLowerCase()
|
||
|
Collector.isValidCommandName(cmd)
|
||
|
if (this.getAvailableCommands(cmd).length > 0) return false
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
////////////////////////////////////////////////////////////
|
||
|
//// Logic ////
|
||
|
////////////////////////////////////////////////////////////
|
||
|
|
||
|
/** @name collector */
|
||
|
const collector = new Collector()
|
||
|
|
||
|
collector.registerCommand("help")
|
||
|
.help("Displays this text")
|
||
|
.manual(`Displays a list of useable commands`)
|
||
|
.manual(`you can search/filter for a specific commands by adding a keyword`)
|
||
|
.addArgument(arg => arg.string.setName("filter").min(1).optional())
|
||
|
.exec(async (client, { filter }, reply) => {
|
||
|
/**
|
||
|
* @param {string} str
|
||
|
* @param {number} len
|
||
|
* @private
|
||
|
*/
|
||
|
const fixLen = (str, len) => str + Array(len - str.length).fill(" ").join("")
|
||
|
let length = 0
|
||
|
const cmds = (await collector.getAvailableCommandsByPermission(client))
|
||
|
.filter(cmd => cmd.hasHelp())
|
||
|
.filter(cmd => !filter ||
|
||
|
cmd.getCommandName().match(new RegExp(filter, "i")) ||
|
||
|
cmd.getHelp().match(new RegExp(filter, "i")))
|
||
|
reply(`${format.bold(cmds.length.toString())} Commands found:`)
|
||
|
/**
|
||
|
* @type {string[][]}
|
||
|
* @private
|
||
|
*/
|
||
|
const commands = []
|
||
|
await Promise.all(cmds.map(async cmd => {
|
||
|
if (cmd instanceof CommandGroup) {
|
||
|
if (cmd.getFullCommandName().length > length) length = cmd.getFullCommandName().length
|
||
|
;(await cmd.getAvailableCommands(client)).forEach(sub => {
|
||
|
if (cmd.getFullCommandName().length + sub.getCommandName().length + 1 > length)
|
||
|
length = cmd.getFullCommandName().length + sub.getCommandName().length + 1
|
||
|
commands.push([`${cmd.getFullCommandName()} ${sub.getCommandName()}`, sub.getHelp()])
|
||
|
})
|
||
|
} else {
|
||
|
if (cmd.getFullCommandName().length > length) length = cmd.getFullCommandName().length
|
||
|
commands.push([cmd.getFullCommandName(), cmd.getHelp()])
|
||
|
}
|
||
|
}))
|
||
|
/**
|
||
|
* @type {string[][]}
|
||
|
* @private
|
||
|
*/
|
||
|
const init = [[]]
|
||
|
switch (engine.getBackend()) {
|
||
|
case "discord":
|
||
|
return commands
|
||
|
.map(([cmd, help]) => `${fixLen(cmd, length)} ${help}`)
|
||
|
.reduce((acc, curr) => {
|
||
|
if (acc[acc.length - 1].length + acc.join("\n").length + 6 >= 2000) {
|
||
|
acc[acc.length] = [curr]
|
||
|
} else {
|
||
|
acc[acc.length - 1].push(curr)
|
||
|
}
|
||
|
return acc
|
||
|
}, init)
|
||
|
.forEach(lines => reply(format.code(lines.join("\n"))))
|
||
|
default:
|
||
|
case "ts3":
|
||
|
return commands
|
||
|
.map(([cmd, help]) => `${format.bold(cmd)} ${help}`)
|
||
|
.reduce((acc, curr) => {
|
||
|
if (acc[acc.length - 1].length + acc.join("\n").length + 2 >= 8192) {
|
||
|
acc[acc.length] = [curr]
|
||
|
} else {
|
||
|
acc[acc.length - 1].push(curr)
|
||
|
}
|
||
|
return acc
|
||
|
}, init)
|
||
|
.forEach(lines => reply(`\n${lines.join("\n")}`))
|
||
|
}
|
||
|
})
|
||
|
|
||
|
//creates the man command
|
||
|
collector.registerCommand("man")
|
||
|
.help("Displays detailed help about a command if available")
|
||
|
.manual(`Displays detailed usage help for a specific command`)
|
||
|
.manual(`Arguments with Arrow Brackets (eg. < > ) are mandatory arguments`)
|
||
|
.manual(`Arguments with Square Brackets (eg. [ ] ) are optional arguments`)
|
||
|
.addArgument(arg => arg.string.setName("command").min(1))
|
||
|
.addArgument(arg => arg.string.setName("subcommand").min(1).optional(false, false))
|
||
|
.exec(async (client, { command, subcommand }, reply) => {
|
||
|
/**
|
||
|
* @param {BaseCommand} cmd
|
||
|
* @private
|
||
|
*/
|
||
|
const getManual = cmd => {
|
||
|
if (cmd.hasManual()) return cmd.getManual()
|
||
|
if (cmd.hasHelp()) return cmd.getHelp()
|
||
|
return "No manual available"
|
||
|
}
|
||
|
const cmds = await Collector.checkPermissions(collector.getAvailableCommands(command), client)
|
||
|
if (cmds.length === 0) return reply(`No command with name ${format.bold(command)} found! Did you misstype the command?`)
|
||
|
cmds.forEach(async cmd => {
|
||
|
if (cmd instanceof CommandGroup) {
|
||
|
if (subcommand) {
|
||
|
(await cmd.getAvailableCommands(client, subcommand)).forEach(sub => {
|
||
|
reply(`\n${format.bold("Usage:")} ${cmd.getFullCommandName()} ${sub.getUsage()}\n${getManual(sub)}`)
|
||
|
})
|
||
|
} else {
|
||
|
reply(`${format.bold(cmd.getFullCommandName())} - ${getManual(cmd)}`)
|
||
|
;(await cmd.getAvailableCommands(client)).forEach(sub => {
|
||
|
reply(`${format.bold(`${cmd.getFullCommandName()} ${sub.getUsage()}`)} - ${sub.getHelp()}`)
|
||
|
})
|
||
|
}
|
||
|
} else {
|
||
|
let response = `\nManual for command: ${format.bold(cmd.getFullCommandName())}\n${format.bold("Usage:")} ${cmd.getUsage()}\n${getManual(cmd)}`
|
||
|
if (cmd.getAlias().length > 0) response += `\n${format.bold("Alias")}: ${cmd.getAlias()}`
|
||
|
reply(response)
|
||
|
}
|
||
|
})
|
||
|
})
|
||
|
|
||
|
|
||
|
|
||
|
if (engine.getBackend() === "discord") {
|
||
|
//discord message handler
|
||
|
event.on("message", ev => {
|
||
|
let author = ev.author()
|
||
|
if (!author) {
|
||
|
const id = ev.authorID()
|
||
|
const guild = backend.getBotClientID().split("/")[0]
|
||
|
const clid = `${guild}/${id}`
|
||
|
if (id) {
|
||
|
author = backend.getClientByID(clid)
|
||
|
} else {
|
||
|
debug(DEBUG.VERBOSE)("authorID is undefined")
|
||
|
}
|
||
|
if (!author) {
|
||
|
debug(DEBUG.WARNING)(`could not get author with ID=${id}; replacing client with workaround`)
|
||
|
//simulate the basic functionality of a client object
|
||
|
author = {
|
||
|
// eslint-disable-next-line arrow-parens
|
||
|
chat: (/** @type {string} */ str) => ev.reply(str),
|
||
|
isSelf: () => false,
|
||
|
id: () => clid,
|
||
|
uid: () => clid,
|
||
|
uniqueId: () => clid,
|
||
|
uniqueID: () => clid,
|
||
|
DBID: () => clid,
|
||
|
databaseID: () => clid,
|
||
|
databaseId: () => clid,
|
||
|
type: () => 1,
|
||
|
getURL: () => `<@${id}>`,
|
||
|
name: () => `unknown (ID: ${id})`,
|
||
|
nick: () => `unknown (ID: ${id})`,
|
||
|
phoneticName: () => '',
|
||
|
description: () => '',
|
||
|
getServerGroups: () => [],
|
||
|
getChannelGroup: () => null,
|
||
|
getChannels: () => [],
|
||
|
getAudioChannel: () => null,
|
||
|
// eslint-disable-next-line arrow-parens
|
||
|
equals: (/** @type {Client} */ client) => {
|
||
|
const uid = client.uid().split("/")
|
||
|
if (uid.length === 2) {
|
||
|
return uid[2] === id
|
||
|
} else {
|
||
|
return client.uid() === clid
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
messageHandler({
|
||
|
text: ev.content(),
|
||
|
channel: ev.channel(),
|
||
|
client: author,
|
||
|
mode: ev.guildID() ? 2 : 1,
|
||
|
message: ev
|
||
|
})
|
||
|
})
|
||
|
} else {
|
||
|
//teamspeak message handler
|
||
|
event.on("chat", messageHandler)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handles chat/message events
|
||
|
* @private
|
||
|
* @param {MessageEvent} ev
|
||
|
*/
|
||
|
function messageHandler(ev) {
|
||
|
if (typeof engine.getIgnoreCommandsFromPrivateChat === "function") {
|
||
|
//check ignore private chat
|
||
|
if (ev.mode === 1 && engine.getIgnoreCommandsFromPrivateChat())
|
||
|
return debug(DEBUG.VERBOSE)("ignoring private chat due to sinusbot instance settings")
|
||
|
//check ignore channel chat
|
||
|
if (ev.mode === 2 && engine.getIgnoreCommandsFromChannelChat())
|
||
|
return debug(DEBUG.VERBOSE)("ignoring channel chat due to sinusbot instance settings")
|
||
|
//check ignore server chat
|
||
|
if (ev.mode === 3 && engine.getIgnoreCommandsFromServerChat())
|
||
|
return debug(DEBUG.VERBOSE)("ignoring server chat due to sinusbot instance settings")
|
||
|
}
|
||
|
//do not do anything when the client is undefined
|
||
|
if (!ev.client) return debug(DEBUG.WARNING)("client is undefined")
|
||
|
//do not do anything when the bot sends a message
|
||
|
if (ev.client.isSelf()) return debug(DEBUG.VERBOSE)("Will not handle messages from myself")
|
||
|
//check if it is a possible command
|
||
|
if (!collector.isPossibleCommand(ev.text)) return debug(DEBUG.VERBOSE)("No possible valid command found!")
|
||
|
//get the basic command with arguments and command splitted
|
||
|
const match = ev.text.match(new RegExp(`^(?<command>\\S*)\\s*(?<args>.*)\\s*$`, "s"))
|
||
|
if (!match || !match.groups) throw new Error(`command regex missmatch for '${ev.text}'`)
|
||
|
const { command, args } = match.groups
|
||
|
//check if command exists
|
||
|
const commands = collector.getAvailableCommandsWithPrefix(command)
|
||
|
if (commands.length === 0) {
|
||
|
//depending on the config setting return without error
|
||
|
if (NOT_FOUND_MESSAGE !== "0") return
|
||
|
//send the not found message
|
||
|
return Collector.getReplyOutput(ev)(`There is no enabled command named ${format.bold(command.toLowerCase())}, check ${format.bold(`${Collector.getCommandPrefix()}help`)} to get a list of available commands!`)
|
||
|
}
|
||
|
//handle every available command, should actually be only one command
|
||
|
commands.forEach(async cmd => {
|
||
|
const start = Date.now()
|
||
|
try {
|
||
|
debug(DEBUG.INFO)(`${ev.client.name()} (${ev.client.uid()}) used ${cmd.getFullCommandName()}`)
|
||
|
//dispatches the cmd, this will
|
||
|
// - check for permissions
|
||
|
// - parse the arguments
|
||
|
// - dispatch the command
|
||
|
await cmd.dispatch(args, ev)
|
||
|
debug(DEBUG.VERBOSE)(`Command "${cmd.getFullCommandName()}" finnished successfully after ${Date.now() - start}ms`)
|
||
|
//catch errors, parsing errors / permission errors or anything else
|
||
|
} catch (e) {
|
||
|
debug(DEBUG.VERBOSE)(`Command "${cmd.getFullCommandName()}" failed after ${Date.now() - start}ms`)
|
||
|
const reply = Collector.getReplyOutput(ev)
|
||
|
//Handle Command not found Exceptions for CommandGroups
|
||
|
let response = (engine.getBackend() === "ts3" ? "\n" : "")
|
||
|
if (e instanceof CommandNotFoundError) {
|
||
|
response += `${e.message}\n`
|
||
|
response += `For Command usage see ${format.bold(`${Collector.getCommandPrefix()}man ${cmd.getCommandName()}`)}\n`
|
||
|
reply(response)
|
||
|
} else if (e instanceof PermissionError) {
|
||
|
debug(DEBUG.INFO)(`${ev.client.name()} (${ev.client.uid()}) is missing permissions for ${cmd.getFullCommandName()}`)
|
||
|
response += `You do not have permissions to use this command!\n`
|
||
|
response += `To get a list of available commands see ${format.bold(`${Collector.getCommandPrefix()}help`)}`
|
||
|
reply(response)
|
||
|
} else if (e instanceof ParseError) {
|
||
|
response += `Invalid Command usage! For Command usage see ${format.bold(`${Collector.getCommandPrefix()}man ${cmd.getCommandName()}`)}\n`
|
||
|
reply(response)
|
||
|
} else if (e instanceof ThrottleError) {
|
||
|
reply(e.message)
|
||
|
} else if (e instanceof TooManyArgumentsError) {
|
||
|
response += `Too many Arguments received for this Command!\n`
|
||
|
if (e.parseError) {
|
||
|
response += `Argument parsed with an error ${format.bold(e.parseError.argument.getManual())}\n`
|
||
|
response += `Returned with ${format.bold(e.parseError.message)}\n`
|
||
|
}
|
||
|
response += `Invalid Command usage! For Command usage see ${format.bold(`${Collector.getCommandPrefix()}man ${cmd.getCommandName()}`)}`
|
||
|
reply(response)
|
||
|
} else {
|
||
|
reply("An unhandled exception occured, check the sinusbot logs for more informations")
|
||
|
const match = e.stack.match(new RegExp("^(?<type>\\w+): *(?<msg>.+?)\\s+(at .+?\\(((?<script>\\w+):(?<line>\\d+):(?<row>\\d+))\\))", "s"))
|
||
|
if (match) {
|
||
|
const { type, msg, script, line, row } = match.groups
|
||
|
debug(DEBUG.ERROR)(`Unhandled Script Error in Script "${script.endsWith(".js") ? script : `${script}.js`}" on line ${line} at index ${row}`)
|
||
|
debug(DEBUG.ERROR)(`${type}: ${msg}`)
|
||
|
debug(DEBUG.VERBOSE)(e.stack)
|
||
|
} else {
|
||
|
debug(DEBUG.ERROR)("This is _probably_ an Error with a Script which is using command.js!")
|
||
|
debug(DEBUG.ERROR)(e.stack)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
|
||
|
////////////////////////////////////////////////////////////
|
||
|
//// EXPORTS ////
|
||
|
////////////////////////////////////////////////////////////
|
||
|
|
||
|
/**
|
||
|
* @name createCommandGroup
|
||
|
* Creates a new CommandsCommand Instance with the given Command Name
|
||
|
* @param {string} cmd - the command which should be added
|
||
|
* @returns {CommandGroup} returns the created CommandGroup instance
|
||
|
*/
|
||
|
function createCommandGroup(cmd) {
|
||
|
if (!collector.isSaveCommand(cmd)) {
|
||
|
debug(DEBUG.WARNING)(`WARNING there is already a command with name '${cmd}' enabled!`)
|
||
|
debug(DEBUG.WARNING)(`command.js may work not as expected!`)
|
||
|
}
|
||
|
debug(DEBUG.VERBOSE)(`registering commandGroup '${cmd}'`)
|
||
|
return collector.registerCommandGroup(cmd)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @name createCommand
|
||
|
* Creates a new Command Instance with the given Command Name
|
||
|
* @param {string} cmd - the command which should be added
|
||
|
* @returns {Command} returns the created Command
|
||
|
*/
|
||
|
function createCommand(cmd) {
|
||
|
if (!collector.isSaveCommand(cmd)) {
|
||
|
debug(DEBUG.WARNING)(`WARNING there is already a command with name '${cmd}' enabled!`)
|
||
|
debug(DEBUG.WARNING)(`command.js may work not as expected!`)
|
||
|
}
|
||
|
debug(DEBUG.VERBOSE)(`registering command '${cmd}'`)
|
||
|
return collector.registerCommand(cmd)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @name createArgument
|
||
|
* Creates a new Argument Instance
|
||
|
* @param {keyof ArgType} type - the argument type which should be created
|
||
|
* @returns {Argument} returns the created Argument
|
||
|
*/
|
||
|
function createArgument(type) {
|
||
|
const arg = Argument.createArgumentLayer()[type]
|
||
|
if (!(arg instanceof Argument))
|
||
|
throw new Error(`Argument type not found! Available Arguments: ${Object.keys(Argument.createArgumentLayer()).join(", ")}`)
|
||
|
return arg
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @name createGroupedArgument
|
||
|
* creates a new Argument Instance
|
||
|
* @param {"or"|"and"} type the argument type which should be created either "or" or "and" allowed
|
||
|
* @returns {GroupArgument} returns the created Group Argument
|
||
|
*/
|
||
|
function createGroupedArgument(type) {
|
||
|
if (!Object.values(["or", "and"]).includes(type))
|
||
|
throw new Error(`Unexpected GroupArgument type, expected one of ["or", "and"] but got ${type}!`)
|
||
|
return new GroupArgument(type)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @name getCommandPrefix
|
||
|
* retrieves the current Command Prefix
|
||
|
* @returns {string} returns the command prefix
|
||
|
*/
|
||
|
function getCommandPrefix() {
|
||
|
return Collector.getCommandPrefix()
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @name createThrottle
|
||
|
* Creates a new Throttle Instance
|
||
|
* @returns {Throttle} returns the created Throttle
|
||
|
*/
|
||
|
function createThrottle() {
|
||
|
return Collector.createThrottle()
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @name getVersion
|
||
|
* retrieves the semantic version of this script
|
||
|
* @returns {string} returns the semantic version of this script
|
||
|
*/
|
||
|
function getVersion() {
|
||
|
return version
|
||
|
}
|
||
|
|
||
|
module.exports = {
|
||
|
createCommandGroup,
|
||
|
createCommand,
|
||
|
createArgument,
|
||
|
createGroupedArgument,
|
||
|
getCommandPrefix,
|
||
|
createThrottle,
|
||
|
getVersion,
|
||
|
collector
|
||
|
}
|
||
|
})
|