Fix a bug in ShortcutFn

Fix a bug in ShortcutFn

[=[
File: Arguments
This file defines argument types to be used in TODO.
]=]

[=[
Class: Arg
The base argument class. This class is not really meant to be used in practice and is basically a pass through itself,
but <Arg> lays down some important shared functionality for the other argument classes.
]=]
class ulx.Arg
	--Only available statically, meant for internal use only.
	@ShortcutFn = (name, typ, default using nil) =>
		@@__base[name] = (val=default using nil) =>
			ulx.UtilX.CheckArg "#{@@__name}.#{name}", 1, typ, val
			@["_" .. name] = val
			@

	[=[
	Function: Deserialize

	Parameters:
		str - A serialized *string* generated by <Serialize>.

	Returns:
		An instantiation of *<Arg>*. For the derived classes, it returns the appopriate class instantiation with the
		restrictions set according to what the serialized string contained.
	]=]
	@Deserialize: (str using nil) ->
		ulx.Arg!

	[=[
	Variables: Arg Variables
	All these variables are optional, with sensible defaults. *Do not set these directly*. Instead, call the setter
	function of the same name without the underscore. E.G., call `Default(myDefaultValue)`.

		_Default  - A value of *any type*. If an argument is optional and unspecified, this value is used.
		_Optional - A *boolean* of whether or not this argument is optional.
		_Hint - A *string* (usually a word or two) used in help output to give users a general idea of what the argument is for.
		_Help - A *string* used in longer help output to describe the argument.
	]=]
	_Default:  nil
	_Optional: false
	_Hint:     "arg"
	_Help:     "An arbitrary argument"

	@ShortcutFn "Default"
	@ShortcutFn "Optional", "boolean", true
	@ShortcutFn "Hint", "string"
	@ShortcutFn "Help", "string"

	[=[
	Function: UsageShort

	Parameters:
		str - An optional *string* to add the help into. _Defaults to ""_.

	Returns:
		A short, one-line help *string* for using the argument.
	]=]
	UsageShort: (str = "" using nil) =>
		str ..= ", "                   if @_Optional and #str > 0
		str ..= "default #{@_Default}" if @_Optional
		str = ": " .. str              if @_Hint and #str > 0
		str = tostring(@_Hint) .. str  if @_Hint
		str = "<" .. str .. ">"
		str = "[" .. str .. "]"        if @_Optional

		str

	[=[
	Function: UsageLong
	Similar to <UsageShort>, but has no length restrictions on the returned text.

	Parameters:
		str - An optional *string* to add the help into. _Defaults to ""_.

	Returns:
		A full *string* help for using the argument.
	]=]
	UsageLong: (str = "" using nil) =>
		str ..=   "Type:     #{@@__name}"
		str ..= "\nDefault:  #{@_Default} (used if argument is unspecified)" if @_Optional
		str ..= "\nHint:     #{@_Hint}"    if @_Hint
		str ..= "\nHelp:     #{@_Help}"    if @_Help

		str

	[=[
	Function: Completes

	Parameters:
		str - The *string* input for the command so far.

	Returns:
		A *list of strings* to be used as command autocomplete suggestions.
	]=]
	Completes: (str using nil) =>
		{@UsageShort!}

	[=[
	Function: IsValid
	"Valid" for <Arg> is any object that is not nil, or nil if the argument is optional.

	Parameters:
		obj - A value of *any type*.

	Returns:
		A *boolean* on whether the passed object is valid for this argument type.
	]=]
	IsValid: (obj using nil) =>
		obj ~= nil or @_Optional

	[=[
	Function: Parse

	Parameters:
		obj - A value of *any type*.

	Returns:
		The parsed argument for this argument type. For <Arg> (this class), it simply passes back what it receives or the default.
	]=]
	Parse: (obj using nil) =>
		if obj == nil and @_Optional
			return @_Default
		obj

	[=[
	Function: IsPermissible

	Parameters:
		obj - A value of *any type*. For other argument classes, you pass only a parsed object of the appropriate type.
		      Under most circumstances, you'll be passing the output from <Parse> into this function.

	Returns:
		A *boolean* stating whether the passed object meets the restrictions specified by the class configuration.
		If it returns false, a *string* explaining why it is not permissible is passed as the second return value.
	]=]
	IsPermissible: (obj using nil) =>
		true

	[=[
	Function: Serialize

	Returns:
		A serialized *string* version of the restrictions in this class to be used by <Deserialzie>. Since ULX is only
		concerned about saving restrictions created by a user for various commands, nothing is saved by this base class.
	]=]
	Serialize: (using nil) =>
		""

[=[
Class: ArgNum
The argument class used for any numeric data.

Passes:
	A *number* value, defaulting to _0_.
]=]
class ulx.ArgNum extends ulx.Arg
	-- Values from parent that we want to override the defaults for
	_Default: 0
	_Hint:    "number"
	_Help:    "A number argument"

	@ShortcutFn "Default", "number"

	lang = ulx.Lang -- shortcut

	[=[
	Variables: ArgNum Variables
	All these variables are optional, with sensible defaults. See <Arg.Arg Variables> above for an important note.

		_Min   - A *number or nil* specifying the minimum value for the argument.
		_Max   - A *number or nil* specifying the maximum value for the argument.
		_Round - A *number or nil* specifying the digit to round to, as passed to <UtilX.Round>.
		_Hint  - A *string* (usually a word or two) used in help output to give users a general idea of what the argument is for.
	]=]
	_Min:   nil
	_Max:   nil
	_Round: nil

	@ShortcutFn "Min", {"number", "nil"}
	@ShortcutFn "Max", {"number", "nil"}
	@ShortcutFn "Round", {"number", "nil"}, 0

	[=[
	Function: UsageShort
	See <Arg.UsageShort>.
	]=]
	UsageShort: (str = "" using nil) =>
		if @_Min and @_Min == @_Max
			str ..= tostring @_Min

		else
			str ..= @_Min .. "<=" if @_Min
			str ..= "x"
			str ..= "<=" .. @_Max if @_Max

		super\UsageShort str

	[=[
	Function: UsageLong
	See <Arg.UsageLong>.
	]=]
	UsageLong: (str = "" using nil) =>
		str = super\UsageLong(str)

		str ..= "\nMin:      #{@_Min}"   if @_Min
		str ..= "\nMax:      #{@_Max}"   if @_Max
		str ..= "\nRound:    #{@_Round}" if @_Round

		str

	[=[
	Function: IsValid
	See <Arg.IsValid>. For <ArgNum>, "valid" is anything that passes tonumber().
	]=]
	IsValid: (obj using nil) =>
		if obj == nil and @_Optional
			return true
		tonumber(obj) ~= nil

	[=[
	Function: Parse
	See <Arg.Parse>.

	Returns:
		An appropriately rounded *number* or *false* and a *string* message if parsing fails.
	]=]
	Parse: (obj using nil) =>
		local num
		if obj ~= nil
			num = tonumber obj
			if num == nil
				return false, lang.GetMutatedPhrase "ARG_CANNOT_PARSE",
					TYPE: lang.GetPhrase "NUMBER"
					GIVEN: tostring obj
		elseif @_Optional
			num = @_Default
		num = ulx.UtilX.Round num, @_Round if num and @_Round
		return true, num

	[=[
	Function: IsPermissible
	See <Arg.IsPermissible>. For <ArgNum>, checks the bounds of the number.

	Parameters:
		num - A *number* or *nil*.
	]=]
	IsPermissible: (num using nil) =>
		data =
			TYPE: lang.GetPhrase "NUMBER"
			GIVEN: tostring num
			MIN: @_Min
			MAX: @_Max

		return false, lang.GetMutatedPhrase("ARG_NOT_SPECIFIED", data) if num == nil and not @_Optional
		return false, lang.GetMutatedPhrase("ARG_INCORRECT_TYPE", data) if type(num) ~= "number"
		return false, lang.GetMutatedPhrase("ARGNUM_BELOW_MIN", data) if @_Min and num < @_Min
		return false, lang.GetMutatedPhrase("ARGNUM_ABOVE_MAX", data) if @_Max and num > @_Max
		return true

	[=[
	Function: Serialize
	See <Arg.Serialize>.

	Returns:
		A *string* in the format "min_num:max_num".
	]=]
	Serialize: (using nil) =>
		str = ""
		str ..= tostring(@_Min) if @_Min
		str ..= ":"
		str ..= tostring(@_Max) if @_Max
		str

	[=[
	Function: Deserialize
	See <Arg.Deserialize>.
	]=]
	@Deserialize: (str using nil) ->
		splitPt = str\find ":"

		local min, max
		if splitPt
			min = tonumber(str\sub(1, splitPt-1))
			max = tonumber(str\sub(splitPt+1))
		else -- Assume they want it restricted to one value
			min = tonumber(str)
			max = min

		ArgNum!\Min(min)\Max(max)

[=[
Class: ArgTime
The argument class used for a timespan using natural language.

Passes:
	A *number* of seconds, defaulting to _0_.
]=]
class ulx.ArgTime extends ulx.ArgNum
	[=[
	Variables: ArgNum Variables
	All these variables are optional, with sensible defaults.

		Min - A *number, string, or nil* specifying the minimum value for the argument.
		Max - A *number, string, or nil* specifying the maximum value for the argument.
	]=]
	Min: nil
	Max: nil

[=[
Class: ArgString
The argument class used for string arguments.

Passes:
	A *string* value, defaulting to _0_.
]=]
class ulx.ArgString extends ulx.Arg
	[=[
	Variables: ArgString Variables
	All these variables are optional, with sensible defaults.

		Default - A *string*, defaults to _""_. If an argument is optional and unspecified, this value is used.
		RestrictToCompletes - A *boolean*, defaults to _false_.
			If true, the argument passed will /always/ be one of the arguments from the <Completes> table.
		Completes           - A *list of strings or nil* of auto-completes (suggestions) for the argument.

	]=]
	Default:             ""
	RestrictToCompletes: false
	Completes:           nil

[=[
Class: ArgPlayerID
The argument class used for SteamID arguments.

Passes:
	A *table of strings or players* (between one and <MaximumTargets> items).
	Each item is either a valid SteamID or a connected player.
]=]
class ulx.ArgPlayerID extends ulx.Arg
	[=[
	Variables: ArgPlayerID Variables
	All these variables are optional, with sensible defaults.

		Default            - A *string*, defaults to _"^"_ (keyword for the player calling the command).
			If an argument is optional and unspecified, this value is used.
		RestrictTarget     - A *string or nil* specifying the players this argument is allowed to target.
			This is passed to <TODO.GetUser()>. Nil indicates no restriction.
		MaximumTargets     - A *number*, defaulting to _1_, specifying the maximum number of players this argument can target.
		PassPlayerIfActive - A *boolean*. If true, will pass the player object if they are connected.

	]=]
	Default:            "^"
	RestrictTarget:     nil
	MaximumTargets:     1
	PassPlayerIfActive: false

[=[
Class: ArgPlayerActive
The argument class used for player arguments.

Passes:
	A *table of players* (between one and <MaximumTargets> items).
]=]
class ulx.ArgPlayerActive extends ulx.ArgPlayerID

[=[
File: Arguments
This file defines argument types to be used in TODO.
]=]

[=[
Class: Arg
The base argument class. This class is not really meant to be used in practice and is basically a pass through itself,
but <Arg> lays down some important shared functionality for the other argument classes.
]=]
class ulx.Arg
	[=[
	Function: ShortcutFn
	Only available statically, meant for internal use only.
	]=]
	@ShortcutFn = (name, typ, default using nil) ->
		@__base[name] = (val=default using nil) =>
			ulx.UtilX.CheckArg "#{@@__name}.#{name}", 1, typ, val
			@["_" .. name] = val
			@

	[=[
	Variables: Arg Variables
	All these variables are optional, with sensible defaults. *Do not set these directly*. Instead, call the setter
	function of the same name without the underscore. E.G., call `Default(myDefaultValue)`.

		_Default  - A value of *any type*. If an argument is optional and unspecified, this value is used.
		_Optional - A *boolean* of whether or not this argument is optional.
		_Hint - A *string* (usually a word or two) used in help output to give users a general idea of what the argument is for.
		_Help - A *string* used in longer help output to describe the argument.
	]=]
	_Default:  nil
	_Optional: false
	_Hint:     "arg"
	_Help:     "An arbitrary argument"

	@.ShortcutFn "Default"
	@.ShortcutFn "Optional", "boolean", true
	@.ShortcutFn "Hint", "string"
	@.ShortcutFn "Help", "string"

	[=[
	Function: UsageShort

	Parameters:
		str - An optional *string* to add the help into. _Defaults to ""_.

	Returns:
		A short, one-line help *string* for using the argument.
	]=]
	UsageShort: (str = "" using nil) =>
		str ..= ", "                   if @_Optional and #str > 0
		str ..= "default #{@_Default}" if @_Optional
		str = ": " .. str              if @_Hint and #str > 0
		str = tostring(@_Hint) .. str  if @_Hint
		str = "<" .. str .. ">"
		str = "[" .. str .. "]"        if @_Optional

		str

	[=[
	Function: UsageLong
	Similar to <UsageShort>, but has no length restrictions on the returned text.

	Parameters:
		str - An optional *string* to add the help into. _Defaults to ""_.

	Returns:
		A full *string* help for using the argument.
	]=]
	UsageLong: (str = "" using nil) =>
		str ..=   "Type:     #{@@__name}"
		str ..= "\nDefault:  #{@_Default} (used if argument is unspecified)" if @_Optional
		str ..= "\nHint:     #{@_Hint}"    if @_Hint
		str ..= "\nHelp:     #{@_Help}"    if @_Help

		str

	[=[
	Function: Completes

	Parameters:
		str - The *string* input for the command so far.

	Returns:
		A *list of strings* to be used as command autocomplete suggestions.
	]=]
	Completes: (str using nil) =>
		{@UsageShort!}

	[=[
	Function: IsValid
	"Valid" for <Arg> is any object that is not nil, or nil if the argument is optional.

	Parameters:
		obj - A value of *any type*.

	Returns:
		A *boolean* on whether the passed object is valid for this argument type.
	]=]
	IsValid: (obj using nil) =>
		obj ~= nil or @_Optional

	[=[
	Function: Parse

	Parameters:
		obj - A value of *any type*.

	Returns:
		The parsed argument for this argument type. For <ArgNum> (this class), it simply passes back what it receives or the
		default.
	]=]
	Parse: (obj using nil) =>
		if obj == nil and @_Optional
			return @_Default
		obj

	[=[
	Function: IsPermissible

	Parameters:
		obj - A value of *any type*. For other argument classes, you pass only a parsed object of the appropriate type.
		      Under most circumstances, you'll be passing the output from <Parse> into this function.

	Returns:
		A *boolean* stating whether the passed object meets the restrictions specified by the class configuration.
		If it returns false, a *string* explaining why it is not permissible is passed as the second return value.
	]=]
	IsPermissible: (obj using nil) =>
		true

	[=[
	Function: Serialize

	Returns:
		A serialized *string* version of the restrictions in this class to be used by <Deserialzie>. Since ULX is only
		concerned about saving restrictions created by a user for various commands, nothing is saved by this base class.
	]=]
	Serialize: (using nil) =>
		""

	[=[
	Function: Deserialize

	Parameters:
		str - A serialized *string* generated by <Serialize>.

	Returns:
		An instantiation of *<Arg>*. For the derived classes, it returns the appopriate class instantiation with the
		restrictions set according to what the serialized string contained.
	]=]
	@Deserialize: (str using nil) ->
		Arg!

[=[
Class: ArgNum
The argument class used for any numeric data.

Passes:
	A *number* value, defaulting to _0_.
]=]
class ulx.ArgNum extends ulx.Arg
	-- Values from parent that we want to override the defaults for
	_Default: 0
	_Hint:    "number"
	_Help:    "A number argument"

	@.ShortcutFn "Default", "number"

	lang = ulx.Lang -- shortcut

	[=[
	Variables: ArgNum Variables
	All these variables are optional, with sensible defaults. See <Arg.Arg Variables> above for an important note.

		_Min   - A *number or nil* specifying the minimum value for the argument.
		_Max   - A *number or nil* specifying the maximum value for the argument.
		_Round - A *number or nil* specifying the digit to round to, as passed to <UtilX.Round>.
		_Hint  - A *string* (usually a word or two) used in help output to give users a general idea of what the argument is for.
	]=]
	_Min:   nil
	_Max:   nil
	_Round: nil

	@.ShortcutFn "Min", {"number", "nil"}
	@.ShortcutFn "Max", {"number", "nil"}
	@.ShortcutFn "Round", {"number", "nil"}, 0

	[=[
	Function: UsageShort
	See <Arg.UsageShort>.
	]=]
	UsageShort: (str = "" using nil) =>
		if @_Min and @_Min == @_Max
			str ..= tostring @_Min

		else
			str ..= @_Min .. "<=" if @_Min
			str ..= "x"
			str ..= "<=" .. @_Max if @_Max

		super\UsageShort str

	[=[
	Function: UsageLong
	See <Arg.UsageLong>.
	]=]
	UsageLong: (str = "" using nil) =>
		str = super\UsageLong(str)

		str ..= "\nMin:      #{@_Min}"   if @_Min
		str ..= "\nMax:      #{@_Max}"   if @_Max
		str ..= "\nRound:    #{@_Round}" if @_Round

		str

	[=[
	Function: IsValid
	See <Arg.IsValid>. For <ArgNum>, "valid" is anything that passes tonumber().
	]=]
	IsValid: (obj using nil) =>
		if obj == nil and @_Optional
			return true
		tonumber(obj) ~= nil

	[=[
	Function: Parse
	See <Arg.Parse>.

	Returns:
		An appropriately rounded *number* or *false* and a *string* message if parsing fails.
	]=]
	Parse: (obj using nil) =>
		local num
		if obj ~= nil
			num = tonumber obj
			if num == nil
				return false, lang.GetMutatedPhrase "ARG_CANNOT_PARSE",
					TYPE: lang.GetPhrase "NUMBER"
					GIVEN: tostring obj
		elseif @_Optional
			num = @_Default
		num = ulx.UtilX.Round num, @_Round if num and @_Round
		return true, num

	[=[
	Function: IsPermissible
	See <Arg.IsPermissible>. For <ArgNum>, checks the bounds of the number.

	Parameters:
		num - A *number* or *nil*.
	]=]
	IsPermissible: (num using nil) =>
		data =
			TYPE: lang.GetPhrase "NUMBER"
			GIVEN: tostring num
			MIN: @_Min
			MAX: @_Max

		return false, lang.GetMutatedPhrase("ARG_NOT_SPECIFIED", data) if num == nil and not @_Optional
		return false, lang.GetMutatedPhrase("ARG_INCORRECT_TYPE", data) if type(num) ~= "number"
		return false, lang.GetMutatedPhrase("ARGNUM_BELOW_MIN", data) if @_Min and num < @_Min
		return false, lang.GetMutatedPhrase("ARGNUM_ABOVE_MAX", data) if @_Max and num > @_Max
		return true

	[=[
	Function: Serialize
	See <Arg.Serialize>.

	Returns:
		A *string* in the format "min_num:max_num".
	]=]
	Serialize: (using nil) =>
		str = ""
		str ..= tostring(@_Min) if @_Min
		str ..= ":"
		str ..= tostring(@_Max) if @_Max
		str

	[=[
	Function: Deserialize
	See <Arg.Deserialize>.
	]=]
	@Deserialize: (str using nil) ->
		splitPt = str\find ":"

		local min, max
		if splitPt
			min = tonumber(str\sub(1, splitPt-1))
			max = tonumber(str\sub(splitPt+1))
		else -- Assume they want it restricted to one value
			min = tonumber(str)
			max = min

		ArgNum!\Min(min)\Max(max)

[=[
Class: ArgTime
The argument class used for a timespan using natural language.

Passes:
	A *number* of seconds, defaulting to _0_.
]=]
class ulx.ArgTime extends ulx.ArgNum
	[=[
	Variables: ArgNum Variables
	All these variables are optional, with sensible defaults.

		Min - A *number, string, or nil* specifying the minimum value for the argument.
		Max - A *number, string, or nil* specifying the maximum value for the argument.
	]=]
	Min: nil
	Max: nil

[=[
Class: ArgString
The argument class used for string arguments.

Passes:
	A *string* value, defaulting to _0_.
]=]
class ulx.ArgString extends ulx.Arg
	[=[
	Variables: ArgString Variables
	All these variables are optional, with sensible defaults.

		Default - A *string*, defaults to _""_. If an argument is optional and unspecified, this value is used.
		RestrictToCompletes - A *boolean*, defaults to _false_.
			If true, the argument passed will /always/ be one of the arguments from the <Completes> table.
		Completes           - A *list of strings or nil* of auto-completes (suggestions) for the argument.

	]=]
	Default:             ""
	RestrictToCompletes: false
	Completes:           nil

[=[
Class: ArgPlayerID
The argument class used for SteamID arguments.

Passes:
	A *table of strings or players* (between one and <MaximumTargets> items).
	Each item is either a valid SteamID or a connected player.
]=]
class ulx.ArgPlayerID extends ulx.Arg
	[=[
	Variables: ArgPlayerID Variables
	All these variables are optional, with sensible defaults.

		Default            - A *string*, defaults to _"^"_ (keyword for the player calling the command).
			If an argument is optional and unspecified, this value is used.
		RestrictTarget     - A *string or nil* specifying the players this argument is allowed to target.
			This is passed to <TODO.GetUser()>. Nil indicates no restriction.
		MaximumTargets     - A *number*, defaulting to _1_, specifying the maximum number of players this argument can target.
		PassPlayerIfActive - A *boolean*. If true, will pass the player object if they are connected.

	]=]
	Default:            "^"
	RestrictTarget:     nil
	MaximumTargets:     1
	PassPlayerIfActive: false

[=[
Class: ArgPlayerActive
The argument class used for player arguments.

Passes:
	A *table of players* (between one and <MaximumTargets> items).
]=]
class ulx.ArgPlayerActive extends ulx.ArgPlayerID


debug messages

debug messages

socket = require 'cqueues.socket'

colors = {
	[0]:  15, -- white
	[1]:  0,  -- black
	[2]:  4,  -- blue
	[3]:  2,  -- green
	[4]:  1,  -- red
	[5]:  3,  -- brown
	[6]:  5,  -- purple
	[7]:  3,  -- orange
	[8]:  11, -- yellow
	[9]:  10, -- light green
	[10]: 6,  -- teal
	[11]: 14, -- cyan
	[12]: 12, -- light blue
	[13]: 13, -- pink
	[14]: 8,  -- gray
	[15]: 7  -- light gray
}
class Logger
	helpers: {
		error: '\00304',
		reset: '\003',
		warn:  '\00308',
		okay:  '\00303'
	}
	print_bare: (line)->
		print(line\gsub('\003(%d%d?),(%d%d?)', (fg, bg)->
			fg, bg = tonumber(fg), tonumber(bg)
			if colors[fg] and colors[bg]
				return '\27[38;5;' .. colors[fg] .. ';48;5;' .. colors[bg] .. 'm'
		)\gsub('\003(%d%d?)', (fg)->
			fg = tonumber(fg)
			if colors[fg]
				return '\27[38;5;' .. colors[fg] .. 'm'
		)\gsub('\003', ()->
			return '\27[0m'
		) .. '\27[0m')
	log: (line)->
		Logger.print_bare os.date('[%X]')\gsub('.', (ch)->
			if ch\match '[%[%]:]'
				return '\00311' .. ch .. '\003'
			else
				return '\00315' .. ch .. '\003'
		) .. ' ' .. line

class IRCConnection
	new: (server, port=6667, config={})=>
		assert(server)
		@config = :server, :port, :config
		for k, v in pairs(config)
			@config[k] = v

		@handlers = {}
		@senders  = {}
		@server   = {}

	add_handler: (id, handler)=>
		if not @handlers[id]
			@handlers[id] = {handler}
		else
			table.insert @handlers[id], handler

	add_sender: (id, sender)=>
		assert not @senders[id], "Sender already exists: " .. id
		@senders[id] = sender

	load_modules: (modules)=>
		if modules.senders
			for id, sender in pairs modules.senders
				@\add_sender id, sender
		if modules.handlers
			for id, handler in pairs modules.handlers
				@\add_handler id, handler

	connect: ()=>
		if @socket
			@socket\shutdown!
		host = @config.server
		port = @config.port
		Logger.log Logger.helpers.warn .. '--- Connecting...'
		@socket = assert socket.connect{:host, :port}
		Logger.log Logger.helpers.okay .. '--- Connected'
		if @config.ssl
			@socket\starttls!
		nick = @config.nick or 'Moonmoon'
		user = @config.username or 'moon'
		real = @config.realname or 'Moon Moon: MoonScript IRC Bot'
		@\send_raw ('NICK %s')\format nick
		@\send_raw ('USER %s * * :%s')\format user, real
		Logger.log Logger.helpers.okay .. '--- Sent authentication data'

	send_raw: (...)=>
		@socket\write table.concat({...}, ' ') .. '\n'

	send: (name, pattern, ...)=>
		@senders[name] pattern\format ...

	parse: (message)=>
		prefix_end = 0
		prefix = nil
		if message\sub(1, 1) == ":"
			prefix_end = message\find " "
			prefix = message\sub 2, message\find(" ") - 1

		trailing = nil
		tstart = message\find " :"
		if tstart
			trailing = message\sub tstart + 1
		else
			tstart = #message

		rest = ((segment)->
			t = {}
			for word in segment\gmatch "%S+"
				table.insert t, word

			return t
		)(message\sub prefix_end + 1, tstart)

		command = rest[1]
		table.remove(rest, 1)
		return prefix, command, rest, trailing

	process: (line)=>
		prefix, command, args, rest = @\parse line
		if not @handlers[command]
			return
		if os.getenv 'IRC_DEBUG'
			Logger.log Logger.helpers.okay .. ' --- | Running trigger: ' .. command
			Logger.log Logger.helpers.okay .. ' --- |\\ Line: ' .. line
			if prefix
				Logger.log Logger.helpers.okay .. ' --- |\\ Prefix: ' .. prefix
			if #args > 0
				Logger.log Logger.helpers.okay .. ' --- |\\ Arguments: ' .. table.concat(args, ', ')
			if rest
				Logger.log Logger.helpers.okay .. ' ---  \\ Trailing: ' .. rest
		for _, handler in pairs @handlers[command]
			ok, err = pcall handler, @, prefix, args, rest
			if not ok
				Logger.log Logger.helpers.warn .. ' *** ' .. err

	loop: ()=>
		local line
		print_error =(err)->
			Logger.log "Error: " .. err .. " (" .. line .. ")"

		for received_line in @socket\lines! do
			line = received_line
			xpcall @process, print_error, @, received_line

return :IRCConnection, :Logger

socket = require 'cqueues.socket'

colors = {
	[0]:  15, -- white
	[1]:  0,  -- black
	[2]:  4,  -- blue
	[3]:  2,  -- green
	[4]:  1,  -- red
	[5]:  3,  -- brown
	[6]:  5,  -- purple
	[7]:  3,  -- orange
	[8]:  11, -- yellow
	[9]:  10, -- light green
	[10]: 6,  -- teal
	[11]: 14, -- cyan
	[12]: 12, -- light blue
	[13]: 13, -- pink
	[14]: 8,  -- gray
	[15]: 7  -- light gray
}
class Logger
	helpers: {
		error: '\00304',
		reset: '\003',
		warn:  '\00308',
		okay:  '\00303'
	}
	print_bare: (line)->
		print(line\gsub('\003(%d%d?),(%d%d?)', (fg, bg)->
			fg, bg = tonumber(fg), tonumber(bg)
			if colors[fg] and colors[bg]
				return '\27[38;5;' .. colors[fg] .. ';48;5;' .. colors[bg] .. 'm'
		)\gsub('\003(%d%d?)', (fg)->
			fg = tonumber(fg)
			if colors[fg]
				return '\27[38;5;' .. colors[fg] .. 'm'
		)\gsub('\003', ()->
			return '\27[0m'
		) .. '\27[0m')
	log: (line)->
		Logger.print_bare os.date('[%X]')\gsub('.', (ch)->
			if ch\match '[%[%]:]'
				return '\00311' .. ch .. '\003'
			else
				return '\00315' .. ch .. '\003'
		) .. ' ' .. line

class IRCConnection
	new: (server, port=6667, config={})=>
		assert(server)
		@config = :server, :port, :config
		for k, v in pairs(config)
			@config[k] = v

		@handlers = {}
		@senders  = {}
		@server   = {}

	add_handler: (id, handler)=>
		if not @handlers[id]
			@handlers[id] = {handler}
		else
			table.insert @handlers[id], handler

	add_sender: (id, sender)=>
		assert not @senders[id], "Sender already exists: " .. id
		@senders[id] = sender

	load_modules: (modules)=>
		if modules.senders
			for id, sender in pairs modules.senders
				@\add_sender id, sender
		if modules.handlers
			for id, handler in pairs modules.handlers
				@\add_handler id, handler

	connect: ()=>
		if @socket
			@socket\shutdown!
		host = @config.server
		port = @config.port
		Logger.log Logger.helpers.warn .. '--- Connecting...'
		@socket = assert socket.connect{:host, :port}
		Logger.log Logger.helpers.okay .. '--- Connected'
		if @config.ssl
			@socket\starttls!
		nick = @config.nick or 'Moonmoon'
		user = @config.username or 'moon'
		real = @config.realname or 'Moon Moon: MoonScript IRC Bot'
		@\send_raw ('NICK %s')\format nick
		@\send_raw ('USER %s * * :%s')\format user, real
		Logger.log Logger.helpers.okay .. '--- Sent authentication data'

	send_raw: (...)=>
		@socket\write table.concat({...}, ' ') .. '\n'

	send: (name, pattern, ...)=>
		@senders[name] pattern\format ...

	parse: (message)=>
		prefix_end = 0
		prefix = nil
		if message\sub(1, 1) == ":"
			prefix_end = message\find " "
			prefix = message\sub 2, message\find(" ") - 1

		trailing = nil
		tstart = message\find " :"
		if tstart
			trailing = message\sub tstart + 1
		else
			tstart = #message

		rest = ((segment)->
			t = {}
			for word in segment\gmatch "%S+"
				table.insert t, word

			return t
		)(message\sub prefix_end + 1, tstart)

		command = rest[1]
		table.remove(rest, 1)
		return prefix, command, rest, trailing

	process: (line)=>
		prefix, command, args, rest = @\parse line
		if not @handlers[command]
			return
		for _, handler in pairs @handlers[command]
			ok, err = pcall handler, @, prefix, args, rest
			if not ok
				Logger.log Logger.helpers.warn .. ' *** ' .. err

	loop: ()=>
		local line
		print_error =(err)->
			Logger.log "Error: " .. err .. " (" .. line .. ")"

		for received_line in @socket\lines! do
			line = received_line
			xpcall @process, print_error, @, received_line

return :IRCConnection, :Logger


irc: Change IRCClient\send; don't format

--- IRC client class
-- @classmod IRCClient

socket = require 'cqueues.socket'
Logger = require 'logger'

escapers =  {['s']: ' ', ['r']: '\r', ['n']: '\n', [';']: ';'}

class IRCClient
	handlers: {}
	senders:  {}
	hooks:    {}
	commands: {}

	default_config: {
		prefix: "!"
	}

	--- Generate a new IRCClient
	-- @tparam string server IRC server name
	-- @tparam number port IRC port number
	-- @tparam table config Default configuration
	new: (server, port=6697, config=@default_config)=>
		assert(server)
		@config = :server, :port, :config, ssl: port == 6697
		for k, v in pairs(config)
			@config[k] = v

		@commands = setmetatable {}, __index: IRCClient.commands
		@hooks    = {}
		@handlers = {}
		@senders  = setmetatable {}, __index: IRCClient.senders
		@server   = {}

	--- Add an IRC bot command
	-- @tparam string name Bot command name
	-- @tparam function command Function for handling command
	add_command: (name, command)=>
		@commands[name] = command

	--- Add a client processing hook
	-- @tparam string id Name of event to hook into
	-- @tparam function hook Processor for hook event
	add_hook: (id, hook)=>
		if not @hooks[id]
			@hooks[id] = {hook}
		else
			table.insert @hooks[id], hook

	--- Add an IRC command handler
	-- @tparam string id IRC command ID (numerics MUST be strings)
	-- @tparam function handler IRC command processor
	add_handler: (id, handler)=>
		if not @handlers[id]
			@handlers[id] = {handler}
		else
			table.insert @handlers[id], handler

	--- Add an IRC command sending handler
	-- @tparam string id IRC command ID ("PRIVMSG", "JOIN", etc.)
	-- @tparam function sender Function to handle message to be sent
	add_sender: (id, sender)=>
		assert not @senders[id], "Sender already exists: " .. id
		@senders[id] = sender

	--- Append modules to the IRCClient based on a table
	-- @tparam table modules Table with senders/handlers/hooks fields
	load_modules: (modules)=>
		if modules.senders
			for id, sender in pairs modules.senders
				@add_sender id, sender
		if modules.handlers
			for id, handler in pairs modules.handlers
				@add_handler id, handler
		if modules.hooks
			for id, hook in pairs modules.hooks
				@add_hook id, hook

	--- Reset all modules in the IRCClient
	clear_modules: ()=>
		@senders = {}
		@handlers = {}
		@hooks = {}
		@commands = {}

	--- Connect to the IRC server specified in the configuration
	connect: ()=>
		if @socket
			Logger.debug "Shutting down socket: #{tostring(@socket)}"
			@socket\shutdown!
		host = @config.server
		port = @config.port
		ssl  = @config.ssl
		debug_msg = ('Connecting... {host: "%s", port: "%s"}')\format host, port
		--
		@config.nick = 'Moon-Moon' if not @config.nick
		@config.username = 'Mooooon' if not @config.username
		@config.realname = 'Moon Moon: MoonScript IRC Bot' if not @config.realname
		--
		Logger.debug debug_msg, Logger.level.warn .. '--- Connecting...'
		@socket = assert socket.connect{:host, :port}
		if ssl
			Logger.debug 'Starting TLS exchange...'
			@socket\starttls!
			Logger.debug 'Started TLS exchange'
		Logger.print Logger.level.okay .. '--- Connected'
		@fire_hook 'CONNECT'
		nick = @config.nick
		user = @config.username
		real = @config.realname
		pass = @config.password
		Logger.print Logger.level.warn .. '--- Sending authentication data'
		@send_raw ('NICK %s')\format nick
		if pass and ssl
			Logger.debug '*** Sending password'
			@send_raw ('PASS :%s')\format pass
		elseif pass
			Logger.print Logger.level.error .. '*** Not sending password: TLS not enabled ***'
		@send_raw ('USER %s * * :%s')\format user, real
		debug_msg = ('Sent authentication data: {nickname: %s, username: %s, realname: %s}')\format nick, user, real
		Logger.debug debug_msg, Logger.level.okay .. '--- Sent authentication data'

	--- Disconnect from the current IRC server
	disconnect: ()=>
		@socket\shutdown! if @socket
		@fire_hook 'DISCONNECT'

	--- Send a raw line to the currently connected IRC server
	-- @param ... List of strings, concatenated using spaces
	send_raw: (...)=>
		@socket\write table.concat({...}, ' ') .. '\n'
		Logger.debug '==> ' .. table.concat {...}, ' '

	--- Send a command using a builtin sender configured with @add_sender
	-- @tparam string name Name of sender to use
	-- @param ... List of arguments to be passed to sender
	-- @see IRCClient\add_sender
	send: (name, ...)=>
		@send_raw @senders[name] self, ...

	date_pattern: "(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+).(%d+)Z"

	--- Parse the time of an ISO 8601 compliant string
	-- @tparam string date ISO 8601 compliant YYYY-MM-DDThh:mm:ssZ date
	-- @treturn number Seconds from Epoch from configured date
	parse_time: (date)=>
		year, month, day, hour, min, sec, mil = date\match @date_pattern
		return os.time(:year, :month, :day, :hour, :min, :sec) + tonumber(mil) / 1000

	--- Grab tags from an IRCv3 message
	-- @tparam string tag_message Message containing IRCv3 tags
	-- @treturn table Table containing tags, client tags preceded with a "+"
	parse_tags: (tag_message)=>
		local cur_name
		tags = {}
		charbuf = {}
		pos = 1
		while pos < #tag_message do
			if tag_message\match '^\\', pos
				lookahead = tag_message\sub pos+1, pos+1
				charbuf[#charbuf + 1] = escapers[lookahead] or lookahead
				pos += 2
			elseif cur_name
				if tag_message\match "^;", pos
					tags[cur_name] = table.concat charbuf
					cur_name = nil
					charbuf = {}
					pos += 1
				else
					charbuf[#charbuf + 1], pos = tag_message\match "([^\\;]+)()", pos
			else
				if tag_message\match "^=", pos
					if #charbuf > 0
						cur_name = table.concat charbuf
						charbuf = {}
					pos += 1
				elseif tag_message\match "^;", pos
					if #charbuf > 0
						tags[table.concat charbuf] = true
						charbuf = {}
					pos += 1
				else
					charbuf[#charbuf + 1], pos = tag_message\match "([^\\=;]+)()", pos
		return tags

	--- Parse an IRCv3 compatible wire-format string
	-- @tparam string message_with_tags IRCv3 compliant wire-format command
	-- @treturn table User prefix, if sent by a user or server
	-- @treturn string Command sent to client
	-- @treturn table List of arguments, trailing argument not included
	-- @treturn string Trailing possibly space-inclusive message
	-- @treturn table IRCv3 compatible tag list
	-- @see IRCClient\parse_tags
	parse: (message_with_tags)=>
		local message, tags
		if message_with_tags\sub(1, 1) == '@'
			tags = @parse_tags message_with_tags\sub 2, message_with_tags\find(' ') - 1
			message = message_with_tags\sub((message_with_tags\find(' ') + 1))
		else
			message = message_with_tags
		prefix_end = 0
		prefix = nil
		if message\sub(1, 1) == ':'
			prefix_end = message\find ' '
			prefix = message\sub 2, message\find(' ') - 1

		trailing = nil
		tstart = message\find ' :'
		if tstart
			trailing = message\sub tstart + 2
		else
			tstart = #message

		rest = ((segment)->
			t = {}
			for word in segment\gmatch '%S+'
				table.insert t, word

			return t
		)(message\sub prefix_end + 1, tstart)

		command = rest[1]
		table.remove(rest, 1)
		return prefix, command, rest, trailing, tags

	--- Activate hooks configured for the event name
	-- @tparam string hook_name Name of event to "fire"
	-- @treturn boolean True if no errors were encountered, false otherwise
	-- @treturn table Table containing list of errors from hooks
	fire_hook: (hook_name)=>
		if not @hooks[hook_name] and not IRCClient.hooks[hook_name]
			return false
		has_errors = false
		errors = {}
		if IRCClient.hooks[hook_name] then
			for _, hook in pairs IRCClient.hooks[hook_name]
				Logger.debug Logger.level.warn .. '--- Running global hook: ' .. hook_name
				ok, err = pcall hook, @
				if not ok
					has_errors = true if not has_errors
					table.append(errors, err)
					Logger.print Logger.level.error .. '*** ' .. err
		if @hooks[hook_name] then
			for _, hook in pairs @hooks[hook_name]
				Logger.debug Logger.level.warn .. '--- Running hook: ' .. hook_name
				ok, err = pcall hook, @
				if not ok
					has_errors = true if not has_errors
					table.append(errors, err)
					Logger.print Logger.level.error .. '*** ' .. err
		return has_errors, errors

	--- Run handlers for an unparsed command
	-- @tparam string line Incoming wire-string formatted line from IRC server
	process: (line)=>
		prefix, command, args, rest, tags = @parse line
		Logger.debug Logger.level.warn .. '--- | Line: ' .. line
		Logger.debug Logger.level.okay .. '--- |\\ Running trigger: ' .. Logger.level.warn .. command
		if prefix
			Logger.debug Logger.level.okay .. '--- |\\ Prefix: ' .. prefix
		if #args > 0
			Logger.debug Logger.level.okay .. '--- |\\ Arguments: ' .. table.concat(args, ', ')
		if rest
			Logger.debug Logger.level.okay .. '--- |\\ Trailing: ' .. rest
		if not @handlers[command] and not IRCClient.handlers[command]
			Logger.debug Logger.level.error .. "*** Handler not found for #{command}"
			return
		if IRCClient.handlers[command]
			for _, handler in pairs IRCClient.handlers[command]
				ok, err, tb = xpcall handler, self\log_traceback, self,
					prefix, args, rest, tags
				if not ok
					@log tb
					@log Logger.level.error .. '*** ' .. err
		if @handlers[command]
			for _, handler in pairs @handlers[command]
				ok, err = xpcall handler, (()-> @log_traceback!), @,
					prefix, args, rest, tags
				if not ok
					Logger.print Logger.level.error .. '*** ' .. err

	--- Iterate over lines from a server and handle errors appropriately
	loop: ()=>
		local line
		print_error = (err)->
			Logger.debug "Error: " .. err .. " (" .. line .. ")"

		for received_line in @socket\lines! do
			line = received_line
			xpcall @process, print_error, @, received_line

	--- Print a traceback using the internal logging mechanism
	-- @see IRCClient\log
	log_traceback: (err)=>
		@log Logger.level.error .. '*** ' .. err
		@log debug.traceback()
		@log Logger.level.error .. '---'

	--- Log message from IRC server (used in plugins)
	-- @tparam string input Line to print, IRC color formatted
	-- @see logger
	log: (input)=>
		for line in input\gmatch "[^\r\n]+"
			Logger.print '\00311(\003' .. (@server.caps and
				@server.caps['NETWORK'] or @config.server) ..
				"\00311)\003 #{line}"

return {:IRCClient}

--- IRC client class
-- @classmod IRCClient

socket = require 'cqueues.socket'
Logger = require 'logger'

escapers =  {['s']: ' ', ['r']: '\r', ['n']: '\n', [';']: ';'}

class IRCClient
	handlers: {}
	senders:  {}
	hooks:    {}
	commands: {}

	default_config: {
		prefix: "!"
	}

	--- Generate a new IRCClient
	-- @tparam string server IRC server name
	-- @tparam number port IRC port number
	-- @tparam table config Default configuration
	new: (server, port=6697, config=@default_config)=>
		assert(server)
		@config = :server, :port, :config, ssl: port == 6697
		for k, v in pairs(config)
			@config[k] = v

		@commands = setmetatable {}, __index: IRCClient.commands
		@hooks    = {}
		@handlers = {}
		@senders  = setmetatable {}, __index: IRCClient.senders
		@server   = {}

	--- Add an IRC bot command
	-- @tparam string name Bot command name
	-- @tparam function command Function for handling command
	add_command: (name, command)=>
		@commands[name] = command

	--- Add a client processing hook
	-- @tparam string id Name of event to hook into
	-- @tparam function hook Processor for hook event
	add_hook: (id, hook)=>
		if not @hooks[id]
			@hooks[id] = {hook}
		else
			table.insert @hooks[id], hook

	--- Add an IRC command handler
	-- @tparam string id IRC command ID (numerics MUST be strings)
	-- @tparam function handler IRC command processor
	add_handler: (id, handler)=>
		if not @handlers[id]
			@handlers[id] = {handler}
		else
			table.insert @handlers[id], handler

	--- Add an IRC command sending handler
	-- @tparam string id IRC command ID ("PRIVMSG", "JOIN", etc.)
	-- @tparam function sender Function to handle message to be sent
	add_sender: (id, sender)=>
		assert not @senders[id], "Sender already exists: " .. id
		@senders[id] = sender

	--- Append modules to the IRCClient based on a table
	-- @tparam table modules Table with senders/handlers/hooks fields
	load_modules: (modules)=>
		if modules.senders
			for id, sender in pairs modules.senders
				@add_sender id, sender
		if modules.handlers
			for id, handler in pairs modules.handlers
				@add_handler id, handler
		if modules.hooks
			for id, hook in pairs modules.hooks
				@add_hook id, hook

	--- Reset all modules in the IRCClient
	clear_modules: ()=>
		@senders = {}
		@handlers = {}
		@hooks = {}
		@commands = {}

	--- Connect to the IRC server specified in the configuration
	connect: ()=>
		if @socket
			Logger.debug "Shutting down socket: #{tostring(@socket)}"
			@socket\shutdown!
		host = @config.server
		port = @config.port
		ssl  = @config.ssl
		debug_msg = ('Connecting... {host: "%s", port: "%s"}')\format host, port
		--
		@config.nick = 'Moon-Moon' if not @config.nick
		@config.username = 'Mooooon' if not @config.username
		@config.realname = 'Moon Moon: MoonScript IRC Bot' if not @config.realname
		--
		Logger.debug debug_msg, Logger.level.warn .. '--- Connecting...'
		@socket = assert socket.connect{:host, :port}
		if ssl
			Logger.debug 'Starting TLS exchange...'
			@socket\starttls!
			Logger.debug 'Started TLS exchange'
		Logger.print Logger.level.okay .. '--- Connected'
		@fire_hook 'CONNECT'
		nick = @config.nick
		user = @config.username
		real = @config.realname
		pass = @config.password
		Logger.print Logger.level.warn .. '--- Sending authentication data'
		@send_raw ('NICK %s')\format nick
		if pass and ssl
			Logger.debug '*** Sending password'
			@send_raw ('PASS :%s')\format pass
		elseif pass
			Logger.print Logger.level.error .. '*** Not sending password: TLS not enabled ***'
		@send_raw ('USER %s * * :%s')\format user, real
		debug_msg = ('Sent authentication data: {nickname: %s, username: %s, realname: %s}')\format nick, user, real
		Logger.debug debug_msg, Logger.level.okay .. '--- Sent authentication data'

	--- Disconnect from the current IRC server
	disconnect: ()=>
		@socket\shutdown! if @socket
		@fire_hook 'DISCONNECT'

	--- Send a raw line to the currently connected IRC server
	-- @param ... List of strings, concatenated using spaces
	send_raw: (...)=>
		@socket\write table.concat({...}, ' ') .. '\n'
		Logger.debug '==> ' .. table.concat {...}, ' '

	--- Send a command using a builtin sender configured with @add_sender
	-- @tparam string name Name of sender to use
	-- @tparam string pattern Pattern to use for formatting
	-- @param ... List of strings to use for formatted string
	-- @see IRCClient\add_sender
	send: (name, pattern, ...)=>
		@send_raw @senders[name] pattern\format ...

	date_pattern: "(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+).(%d+)Z"

	--- Parse the time of an ISO 8601 compliant string
	-- @tparam string date ISO 8601 compliant YYYY-MM-DDThh:mm:ssZ date
	-- @treturn number Seconds from Epoch from configured date
	parse_time: (date)=>
		year, month, day, hour, min, sec, mil = date\match @date_pattern
		return os.time(:year, :month, :day, :hour, :min, :sec) + tonumber(mil) / 1000

	--- Grab tags from an IRCv3 message
	-- @tparam string tag_message Message containing IRCv3 tags
	-- @treturn table Table containing tags, client tags preceded with a "+"
	parse_tags: (tag_message)=>
		local cur_name
		tags = {}
		charbuf = {}
		pos = 1
		while pos < #tag_message do
			if tag_message\match '^\\', pos
				lookahead = tag_message\sub pos+1, pos+1
				charbuf[#charbuf + 1] = escapers[lookahead] or lookahead
				pos += 2
			elseif cur_name
				if tag_message\match "^;", pos
					tags[cur_name] = table.concat charbuf
					cur_name = nil
					charbuf = {}
					pos += 1
				else
					charbuf[#charbuf + 1], pos = tag_message\match "([^\\;]+)()", pos
			else
				if tag_message\match "^=", pos
					if #charbuf > 0
						cur_name = table.concat charbuf
						charbuf = {}
					pos += 1
				elseif tag_message\match "^;", pos
					if #charbuf > 0
						tags[table.concat charbuf] = true
						charbuf = {}
					pos += 1
				else
					charbuf[#charbuf + 1], pos = tag_message\match "([^\\=;]+)()", pos
		return tags

	--- Parse an IRCv3 compatible wire-format string
	-- @tparam string message_with_tags IRCv3 compliant wire-format command
	-- @treturn table User prefix, if sent by a user or server
	-- @treturn string Command sent to client
	-- @treturn table List of arguments, trailing argument not included
	-- @treturn string Trailing possibly space-inclusive message
	-- @treturn table IRCv3 compatible tag list
	-- @see IRCClient\parse_tags
	parse: (message_with_tags)=>
		local message, tags
		if message_with_tags\sub(1, 1) == '@'
			tags = @parse_tags message_with_tags\sub 2, message_with_tags\find(' ') - 1
			message = message_with_tags\sub((message_with_tags\find(' ') + 1))
		else
			message = message_with_tags
		prefix_end = 0
		prefix = nil
		if message\sub(1, 1) == ':'
			prefix_end = message\find ' '
			prefix = message\sub 2, message\find(' ') - 1

		trailing = nil
		tstart = message\find ' :'
		if tstart
			trailing = message\sub tstart + 2
		else
			tstart = #message

		rest = ((segment)->
			t = {}
			for word in segment\gmatch '%S+'
				table.insert t, word

			return t
		)(message\sub prefix_end + 1, tstart)

		command = rest[1]
		table.remove(rest, 1)
		return prefix, command, rest, trailing, tags

	--- Activate hooks configured for the event name
	-- @tparam string hook_name Name of event to "fire"
	-- @treturn boolean True if no errors were encountered, false otherwise
	-- @treturn table Table containing list of errors from hooks
	fire_hook: (hook_name)=>
		if not @hooks[hook_name] and not IRCClient.hooks[hook_name]
			return false
		has_errors = false
		errors = {}
		if IRCClient.hooks[hook_name] then
			for _, hook in pairs IRCClient.hooks[hook_name]
				Logger.debug Logger.level.warn .. '--- Running global hook: ' .. hook_name
				ok, err = pcall hook, @
				if not ok
					has_errors = true if not has_errors
					table.append(errors, err)
					Logger.print Logger.level.error .. '*** ' .. err
		if @hooks[hook_name] then
			for _, hook in pairs @hooks[hook_name]
				Logger.debug Logger.level.warn .. '--- Running hook: ' .. hook_name
				ok, err = pcall hook, @
				if not ok
					has_errors = true if not has_errors
					table.append(errors, err)
					Logger.print Logger.level.error .. '*** ' .. err
		return has_errors, errors

	--- Run handlers for an unparsed command
	-- @tparam string line Incoming wire-string formatted line from IRC server
	process: (line)=>
		prefix, command, args, rest, tags = @parse line
		Logger.debug Logger.level.warn .. '--- | Line: ' .. line
		Logger.debug Logger.level.okay .. '--- |\\ Running trigger: ' .. Logger.level.warn .. command
		if prefix
			Logger.debug Logger.level.okay .. '--- |\\ Prefix: ' .. prefix
		if #args > 0
			Logger.debug Logger.level.okay .. '--- |\\ Arguments: ' .. table.concat(args, ', ')
		if rest
			Logger.debug Logger.level.okay .. '--- |\\ Trailing: ' .. rest
		if not @handlers[command] and not IRCClient.handlers[command]
			Logger.debug Logger.level.error .. "*** Handler not found for #{command}"
			return
		if IRCClient.handlers[command]
			for _, handler in pairs IRCClient.handlers[command]
				ok, err, tb = xpcall handler, self\log_traceback, self,
					prefix, args, rest, tags
				if not ok
					@log tb
					@log Logger.level.error .. '*** ' .. err
		if @handlers[command]
			for _, handler in pairs @handlers[command]
				ok, err = xpcall handler, (()-> @log_traceback!), @,
					prefix, args, rest, tags
				if not ok
					Logger.print Logger.level.error .. '*** ' .. err

	--- Iterate over lines from a server and handle errors appropriately
	loop: ()=>
		local line
		print_error = (err)->
			Logger.debug "Error: " .. err .. " (" .. line .. ")"

		for received_line in @socket\lines! do
			line = received_line
			xpcall @process, print_error, @, received_line

	--- Print a traceback using the internal logging mechanism
	-- @see IRCClient\log
	log_traceback: (err)=>
		@log Logger.level.error .. '*** ' .. err
		@log debug.traceback()
		@log Logger.level.error .. '---'

	--- Log message from IRC server (used in plugins)
	-- @tparam string input Line to print, IRC color formatted
	-- @see logger
	log: (input)=>
		for line in input\gmatch "[^\r\n]+"
			Logger.print '\00311(\003' .. (@server.caps and
				@server.caps['NETWORK'] or @config.server) ..
				"\00311)\003 #{line}"

return {:IRCClient}