lib/irc: Fix priority for sending authentication

PASS *must* be sent before NICK, at the very least on servers like
Twitch. This commit also removes some debug printing that was left in by
accident previously.

--- IRC client class
-- @classmod IRCClient

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

moonscript = {
	errors: require 'moonscript.errors'
}

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

local IRCClient, ContextTable, priority

priority = {
	HIGH: 1,
	DEFAULT: 2,
	LOW: 3,
}

class ContextTable
	get: (name, opts = {})=>
		:multiple, :index, :uses_priority, :current = opts
		multiple = true if multiple == nil
		uses_priority = true if uses_priority == nil

		local output
		output = current or {} if multiple
		for context, items in pairs(self)
			-- context
			if multiple
				-- add all that exist
				if uses_priority
					for p=priority.HIGH, priority.LOW
						if items[p][name]
							for element in *items[p][name]
								table.insert output, element
				else
					if items[name]
						for element in *items[name]
							table.insert output, element
			else
				-- return the singular if it exists
				if uses_priority
					for p=priority.HIGH, priority.LOW
						return items[p][name] if items[p][name] ~= nil
				else
					return items[name] if items[name] ~= nil

		if multiple and not index
			return output
		if index
			return index\get name, :multiple, :uses_priority, current: output

	remove: (name)=>
		for context_name, tbl in pairs(self)
			tbl[name] = nil

class IRCClient
	commands: ContextTable!
	hooks: ContextTable!
	handlers: ContextTable!
	senders: ContextTable!

	__tostring: => "IRCClient"

	default_config = {
		prefix: "!"
	}

	line_pattern = re.compile [[
		-- tags, command, args
		line <- {|
			{:tags: {| (tags sp)? |} :}
			{:prefix: {| (prefix sp)? |} :}
			{:command: (command / numeric) :}
			{:args: {| (sp arg)* |} :} |}
		tags <- '@' tag (';' tag)*
		tag <- {|
			{:is_client: {'+'} :}? -- check: if tag.is_client
			{:vendor: {[^/]+} '/' :}?
			{:key: {[^=; ]+} -> esc_tag :}
			{:value: ('=' {[^; ]+} -> esc_tag) :}?
		|}
		prefix <- ':' (
			{:nick: {[^ !]+} :} '!'
			{:user: {[^ @]+} :} '@'
			{:host: {[^ ]+} :} /
			{:nick: {[^ ]+} :})
		command <- [A-Za-z]+
		numeric <- %d^+3^-4 -- at most four digits, at least three
		arg <- ':' {.+} / {%S+}
		sp <- %s
	]], esc_tag: (tag)-> tag\gsub "\\(.)", setmetatable({
		[":"]: ";"
		s: " "
		r: "\r"
		n: "\n"
	}, __index: (t, k) -> k)

	--- Get a tag based on search parameters (is_client IS NOT matched by key)
	-- @tparam table tags Tag from IRC parser
	-- @tparam table opts Options to search for (is_client: bool, key: string)
	get_tag: (tags, opts)=>
		:is_client, :key = opts
		for tag in *tags
			continue if is_client ~= nil and tag.is_client ~= is_client
			if key ~= nil
				continue if tag.vendor ~= nil and key ~= "#{tag.vendor}/#{tag.key}"
				continue if tag.vendor == nil and key ~= tag.key
			return tag

		return {}

	--- 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)=>
		assert server
		@config = :server, :port, :config, ssl: port == 6697
		for k, v in pairs default_config
			@config[k] = v
		if config
			for k, v in pairs config
				@config[k] = v

		@commands = ContextTable!
		@hooks    = ContextTable!
		@handlers = ContextTable!
		@senders  = ContextTable!
		@server   = {}

	unpack = unpack or table.unpack

	get_priority = (options, fn)->
		return fn and options.priority or priority.DEFAULT

	handle_options: (options, fn)=>
		unless fn
			fn = options
			options = {}

		if options.async
			return (...)->
				args = {...}
				require("queue")\wrap ->
					@pcall_bare fn, unpack args

		if options.wrap_iter
			tmp_fn = coroutine.wrap fn
			tmp_fn!
			return tmp_fn

		fn

	assert_context: (context_table)=>
		assert @context, "Missing @with_context"
		return context_table[assert @context, "Missing @with_context"]

	--- Change the module context for the current command
	-- @tparam string context module context (typically, the name)
	-- @tparam function fn code to run under context
	with_context: (context, fn)=>
		assert @context == nil, "Already in context: #{@context}"
		@context = context

		self["senders"][context] = {} -- no priority for senders, only one
		for key in *{"hooks", "handlers", "commands"}
			self[key][context] = [{} for p=priority.HIGH, priority.LOW]

		fn self, context
		@context = nil

	--- Add an IRC bot command
	-- @tparam string name Bot command name
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function command Function for handling command
	add_command: (name, options, command)=>
		final_command = command
		commands = @assert_context @commands

		-- `options` is passed, which fills in `command`
		if command
			-- is async, send typing until completed
			if options.async
				-- async, send @+draft/typing
				final_command = (prefix, target, line, ...)=>
					@vars.typing_counter = 0 if not @vars.typing_counter
					@vars.typing_counter += 1
					@send 'TAGMSG', target, "+draft/typing": 'active'
					@pcall command, prefix, target, line, ...
					@vars.typing_counter -= 1
					if @vars.typing_counter == 0
						@send 'TAGMSG', target, "+draft/typing": 'done'
					else
						@send 'TAGMSG', target, "+draft/typing": 'active'

		p = get_priority options, final_command
		commands[p][name] = @handle_options options, final_command

	--- Add a client processing hook
	-- @tparam string name Name of event to hook into
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function hook Processor for hook event
	add_hook: (name, options, hook)=>
		hooks = @assert_context @hooks

		p = get_priority options, hook

		unless hooks[p][name]
			hooks[p][name] = {@handle_options options, hook}
		else
			table.insert hooks[p][name], @handle_options(options, hook)

	--- Add an IRC command handler
	-- @tparam string id IRC command ID (numerics MUST be strings)
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function handler IRC command processor
	add_handler: (id, options, handler)=>
		handlers = @assert_context @handlers

		p = get_priority options, handler

		unless handlers[p][id]
			handlers[p][id] = {@handle_options options, handler}
		else
			table.insert hooks[p][name], @handle_options(options, handler)

	--- Add an IRC command sending handler, does NOT take a priority
	-- @tparam string id IRC command ID ("PRIVMSG", "JOIN", etc.)
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function sender Function to handle message to be sent
	add_sender: (id, options, sender)=>
		senders = @assert_context @senders
		senders[id] = @handle_options(options, sender)

	--- 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'
		if pass and ssl
			Logger.debug '*** Sending password'
			@send_raw 'PASS', pass
		elseif pass
			Logger.print Logger.level.error .. '*** Not sending password: TLS not enabled ***'
		@send_raw 'NICK', nick
		@send_raw 'USER', 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'

	tag_escapes = {
		";": ":"
		" ": "s"
		"\r": "r"
		"\n": "n"
	}

	--- Serialize a value for being sent in a tag
	serialize_tag_string = (key, value)->
		return key if type(value) == "boolean"
		return key .. "=" .. value\gsub "([: \r\n])", (value)-> "\\#{tbl[value]}"

	--- Serialize client->client tags for sending to the server
	-- @param tags Table of tags to serialize
	serialize_tags = (tags)->
		output = [serialize_tag_string(k, v) for k, v in pairs tags]
		return "@#{table.concat output, ';'}"

	--- Send a raw line to the currently connected IRC server
	-- @param ... List of strings, concatenated using spaces
	send_raw: (...)=>
		output = {...}
		arg_count = select("#", ...)
		opts_table = select(arg_count, ...)
		if type(opts_table) == "table"
			-- we have tags, server accepts tags
			if opts_table.tags and next(opts_table.tags) and @server.ircv3_caps["message-tags"]
				table.insert output, 1, serialize_tags opts_table.tags
			-- remove opts_table regardless of caps
			table.remove output, #output
		arg_count = #output
		if output[arg_count]\find " "
			output[arg_count] = ":#{output[arg_count]}"
		@socket\write table.concat(output, ' ') .. '\n'
		Logger.debug '==> ' .. table.concat output, ' '

	--- 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, ...)=>
		sender = @senders\get name, multiple: false, index: IRCClient.senders, uses_priority: false
		input = sender self, ...
		@socket\write input if input

	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

	--- parse an IRC command using line_pattern
	parse: (line)=> line_pattern\match line

	--- 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, ...)=>
		Logger.debug "#{Logger.level.warn}--- Running hooks for #{hook_name}"
		has_run = false
		has_errors = false
		errors = {}

		for _, hook in pairs IRCClient.hooks\get hook_name
			has_run = true unless has_run
			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.insert(errors, err)
		for _, hook in pairs @hooks\get hook_name
			has_run = true unless has_run
			Logger.debug Logger.level.warn .. '--- Running hook: ' .. hook_name
			ok, err = @pcall hook, ...
			if not ok
				has_errors = true if not has_errors
				table.insert(errors, err)

		Logger.debug Logger.level.error .. "*** Hook not found for #{command}" unless has_run

		return has_errors, errors, has_run

	process_line: (line, opts={})=>
		{:tags, :prefix, :command, :args} = @parse line
		opts.line = line
		@process prefix, command, args, tags, opts

	--- Run handlers for an unparsed command
	-- @tparam table prefix Prefix argument from server (nick, user, host)
	-- @tparam table args Argument list from server, including trailing
	-- @tparam table tags Tags list from server
	-- @tparam table opts Optional arguments, in_batch: IRCv3 batching
	process: (prefix, command, args, tags, opts={})=>
		{:in_batch} = opts
		{:nick, :user, :host} = prefix
		if opts.line
			Logger.debug Logger.level.warn .. '--- | Line: ' .. opts.line
			Logger.debug Logger.level.okay .. '--- |\\ Running trigger: ' .. Logger.level.warn .. command
			if nick and user and host
				Logger.debug "#{Logger.level.okay}--- |\\ User: #{nick}!#{user}@#{host}"
			elseif nick
				Logger.debug "#{Logger.level.okay}--- |\\ Nick: #{nick}"
			if #args > 0
				Logger.debug "#{Logger.level.okay}--- |\\ Arguments: #{table.concat args, ' '}"

		has_run = false

		if not in_batch and tags.batch
			-- will return _, _, true if a hook run; this is necessary because
			-- the protocol might send batches even if we don't support them.
			return unless select(2, @fire_hook "BATCH.#{tags.batch}", prefix,
				args, tags, opts)

		for handler in *IRCClient.handlers\get command
			has_run = true unless has_run
			@pcall handler, prefix, args, tags, opts

		for handler in *@handlers\get command
			has_run = true unless has_run
			@pcall handler, prefix, args, tags, opts

		Logger.debug Logger.level.error .. "*** Handler not found for #{command}" unless has_run

	--- Iterate over lines from a server and handle errors appropriately
	loop: ()=>
		for received_line in @socket\lines! do
			@pcall @process_line, received_line, in_batch: false

	--- Call a function and, when failed, print debug information
	-- @tparam function fn Function to be called
	-- @param ... vararg list to pass to function
	pcall: (fn, ...)=>
		ok, err = xpcall fn, self\log_traceback, self, ...
		if not ok
			Logger.debug "Arguments:"
			for arg in *{...}
				Logger.debug tostring arg
		return ok, err

	--- Call a function without self and, when failed, print debug information
	-- @tparam function fn Function to be called
	-- @param ... vararg list to pass to function
	pcall_bare: (fn, ...)=>
		ok, err = xpcall fn, self\log_traceback, ...
		if not ok
			Logger.debug "Arguments:"
			Logger.debug tostring arg for arg in *{...}
		return ok, err

	--- Print a traceback using the internal logging mechanism
	-- @see IRCClient\log
	log_traceback: (err)=>
		err = tostring err
		Logger.debug moonscript.errors.rewrite_traceback debug.traceback!, err
		Logger.debug "#{Logger.level.error} ---"
		return err

	--- 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, :priority}

--- IRC client class
-- @classmod IRCClient

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

moonscript = {
	errors: require 'moonscript.errors'
}

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

local IRCClient, ContextTable, priority

priority = {
	HIGH: 1,
	DEFAULT: 2,
	LOW: 3,
}

class ContextTable
	get: (name, opts = {})=>
		:multiple, :index, :uses_priority, :current = opts
		multiple = true if multiple == nil
		uses_priority = true if uses_priority == nil

		local output
		output = current or {} if multiple
		for context, items in pairs(self)
			-- context
			if multiple
				-- add all that exist
				if uses_priority
					for p=priority.HIGH, priority.LOW
						if items[p][name]
							for element in *items[p][name]
								table.insert output, element
				else
					if items[name]
						for element in *items[name]
							table.insert output, element
			else
				-- return the singular if it exists
				if uses_priority
					for p=priority.HIGH, priority.LOW
						return items[p][name] if items[p][name] ~= nil
				else
					return items[name] if items[name] ~= nil

		if multiple and not index
			return output
		if index
			return index\get name, :multiple, :uses_priority, current: output

	remove: (name)=>
		for context_name, tbl in pairs(self)
			tbl[name] = nil

class IRCClient
	commands: ContextTable!
	hooks: ContextTable!
	handlers: ContextTable!
	senders: ContextTable!

	__tostring: => "IRCClient"

	default_config = {
		prefix: "!"
	}

	line_pattern = re.compile [[
		-- tags, command, args
		line <- {|
			{:tags: {| (tags sp)? |} :}
			{:prefix: {| (prefix sp)? |} :}
			{:command: (command / numeric) :}
			{:args: {| (sp arg)* |} :} |}
		tags <- '@' tag (';' tag)*
		tag <- {|
			{:is_client: {'+'} :}? -- check: if tag.is_client
			{:vendor: {[^/]+} '/' :}?
			{:key: {[^=; ]+} -> esc_tag :}
			{:value: ('=' {[^; ]+} -> esc_tag) :}?
		|}
		prefix <- ':' (
			{:nick: {[^ !]+} :} '!'
			{:user: {[^ @]+} :} '@'
			{:host: {[^ ]+} :} /
			{:nick: {[^ ]+} :})
		command <- [A-Za-z]+
		numeric <- %d^+3^-4 -- at most four digits, at least three
		arg <- ':' {.+} / {%S+}
		sp <- %s
	]], esc_tag: (tag)-> tag\gsub "\\(.)", setmetatable({
		[":"]: ";"
		s: " "
		r: "\r"
		n: "\n"
	}, __index: (t, k) -> k)

	--- Get a tag based on search parameters (is_client IS NOT matched by key)
	-- @tparam table tags Tag from IRC parser
	-- @tparam table opts Options to search for (is_client: bool, key: string)
	get_tag: (tags, opts)=>
		:is_client, :key = opts
		for tag in *tags
			continue if is_client ~= nil and tag.is_client ~= is_client
			if key ~= nil
				continue if tag.vendor ~= nil and key ~= "#{tag.vendor}/#{tag.key}"
				continue if tag.vendor == nil and key ~= tag.key
			return tag

		return {}

	--- 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)=>
		assert server
		@config = :server, :port, :config, ssl: port == 6697
		for k, v in pairs default_config
			@config[k] = v
		if config
			for k, v in pairs config
				@config[k] = v

		@commands = ContextTable!
		@hooks    = ContextTable!
		@handlers = ContextTable!
		@senders  = ContextTable!
		@server   = {}

	unpack = unpack or table.unpack

	get_priority = (options, fn)->
		return fn and options.priority or priority.DEFAULT

	handle_options: (options, fn)=>
		unless fn
			fn = options
			options = {}

		if options.async
			return (...)->
				args = {...}
				require("queue")\wrap ->
					@pcall_bare fn, unpack args

		if options.wrap_iter
			tmp_fn = coroutine.wrap fn
			tmp_fn!
			return tmp_fn

		fn

	assert_context: (context_table)=>
		assert @context, "Missing @with_context"
		return context_table[assert @context, "Missing @with_context"]

	--- Change the module context for the current command
	-- @tparam string context module context (typically, the name)
	-- @tparam function fn code to run under context
	with_context: (context, fn)=>
		assert @context == nil, "Already in context: #{@context}"
		@context = context

		self["senders"][context] = {} -- no priority for senders, only one
		for key in *{"hooks", "handlers", "commands"}
			self[key][context] = [{} for p=priority.HIGH, priority.LOW]

		fn self, context
		@context = nil

	--- Add an IRC bot command
	-- @tparam string name Bot command name
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function command Function for handling command
	add_command: (name, options, command)=>
		final_command = command
		commands = @assert_context @commands

		-- `options` is passed, which fills in `command`
		if command
			-- is async, send typing until completed
			if options.async
				-- async, send @+draft/typing
				final_command = (prefix, target, line, ...)=>
					@vars.typing_counter = 0 if not @vars.typing_counter
					@vars.typing_counter += 1
					@send 'TAGMSG', target, "+draft/typing": 'active'
					@pcall command, prefix, target, line, ...
					@vars.typing_counter -= 1
					print @vars.typing_counter
					if @vars.typing_counter == 0
						@send 'TAGMSG', target, "+draft/typing": 'done'
					else
						@send 'TAGMSG', target, "+draft/typing": 'active'

		p = get_priority options, final_command
		commands[p][name] = @handle_options options, final_command

	--- Add a client processing hook
	-- @tparam string name Name of event to hook into
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function hook Processor for hook event
	add_hook: (name, options, hook)=>
		hooks = @assert_context @hooks

		p = get_priority options, hook

		unless hooks[p][name]
			hooks[p][name] = {@handle_options options, hook}
		else
			table.insert hooks[p][name], @handle_options(options, hook)

	--- Add an IRC command handler
	-- @tparam string id IRC command ID (numerics MUST be strings)
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function handler IRC command processor
	add_handler: (id, options, handler)=>
		handlers = @assert_context @handlers

		p = get_priority options, handler

		unless handlers[p][id]
			handlers[p][id] = {@handle_options options, handler}
		else
			table.insert hooks[p][name], @handle_options(options, handler)

	--- Add an IRC command sending handler, does NOT take a priority
	-- @tparam string id IRC command ID ("PRIVMSG", "JOIN", etc.)
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function sender Function to handle message to be sent
	add_sender: (id, options, sender)=>
		senders = @assert_context @senders
		senders[id] = @handle_options(options, sender)

	--- 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', nick
		if pass and ssl
			Logger.debug '*** Sending password'
			@send_raw 'PASS', pass
		elseif pass
			Logger.print Logger.level.error .. '*** Not sending password: TLS not enabled ***'
		@send_raw 'USER', 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'

	tag_escapes = {
		";": ":"
		" ": "s"
		"\r": "r"
		"\n": "n"
	}

	--- Serialize a value for being sent in a tag
	serialize_tag_string = (key, value)->
		return key if type(value) == "boolean"
		return key .. "=" .. value\gsub "([: \r\n])", (value)-> "\\#{tbl[value]}"

	--- Serialize client->client tags for sending to the server
	-- @param tags Table of tags to serialize
	serialize_tags = (tags)->
		output = [serialize_tag_string(k, v) for k, v in pairs tags]
		return "@#{table.concat output, ';'}"

	--- Send a raw line to the currently connected IRC server
	-- @param ... List of strings, concatenated using spaces
	send_raw: (...)=>
		output = {...}
		arg_count = select("#", ...)
		opts_table = select(arg_count, ...)
		if type(opts_table) == "table"
			-- we have tags, server accepts tags
			if opts_table.tags and next(opts_table.tags) and @server.ircv3_caps["message-tags"]
				table.insert output, 1, serialize_tags opts_table.tags
			-- remove opts_table regardless of caps
			table.remove output, #output
		arg_count = #output
		if output[arg_count]\find " "
			output[arg_count] = ":#{output[arg_count]}"
		@socket\write table.concat(output, ' ') .. '\n'
		Logger.debug '==> ' .. table.concat output, ' '

	--- 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, ...)=>
		sender = @senders\get name, multiple: false, index: IRCClient.senders, uses_priority: false
		input = sender self, ...
		@socket\write input if input

	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

	--- parse an IRC command using line_pattern
	parse: (line)=> line_pattern\match line

	--- 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, ...)=>
		Logger.debug "#{Logger.level.warn}--- Running hooks for #{hook_name}"
		has_run = false
		has_errors = false
		errors = {}

		for _, hook in pairs IRCClient.hooks\get hook_name
			has_run = true unless has_run
			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.insert(errors, err)
		for _, hook in pairs @hooks\get hook_name
			has_run = true unless has_run
			Logger.debug Logger.level.warn .. '--- Running hook: ' .. hook_name
			ok, err = @pcall hook, ...
			if not ok
				has_errors = true if not has_errors
				table.insert(errors, err)

		Logger.debug Logger.level.error .. "*** Hook not found for #{command}" unless has_run

		return has_errors, errors, has_run

	process_line: (line, opts={})=>
		{:tags, :prefix, :command, :args} = @parse line
		opts.line = line
		@process prefix, command, args, tags, opts

	--- Run handlers for an unparsed command
	-- @tparam table prefix Prefix argument from server (nick, user, host)
	-- @tparam table args Argument list from server, including trailing
	-- @tparam table tags Tags list from server
	-- @tparam table opts Optional arguments, in_batch: IRCv3 batching
	process: (prefix, command, args, tags, opts={})=>
		{:in_batch} = opts
		{:nick, :user, :host} = prefix
		if opts.line
			Logger.debug Logger.level.warn .. '--- | Line: ' .. opts.line
			Logger.debug Logger.level.okay .. '--- |\\ Running trigger: ' .. Logger.level.warn .. command
			if nick and user and host
				Logger.debug "#{Logger.level.okay}--- |\\ User: #{nick}!#{user}@#{host}"
			elseif nick
				Logger.debug "#{Logger.level.okay}--- |\\ Nick: #{nick}"
			if #args > 0
				Logger.debug "#{Logger.level.okay}--- |\\ Arguments: #{table.concat args, ' '}"

		has_run = false

		if not in_batch and tags.batch
			-- will return _, _, true if a hook run; this is necessary because
			-- the protocol might send batches even if we don't support them.
			return unless select(2, @fire_hook "BATCH.#{tags.batch}", prefix,
				args, tags, opts)

		for handler in *IRCClient.handlers\get command
			has_run = true unless has_run
			@pcall handler, prefix, args, tags, opts

		for handler in *@handlers\get command
			has_run = true unless has_run
			@pcall handler, prefix, args, tags, opts

		Logger.debug Logger.level.error .. "*** Handler not found for #{command}" unless has_run

	--- Iterate over lines from a server and handle errors appropriately
	loop: ()=>
		for received_line in @socket\lines! do
			@pcall @process_line, received_line, in_batch: false

	--- Call a function and, when failed, print debug information
	-- @tparam function fn Function to be called
	-- @param ... vararg list to pass to function
	pcall: (fn, ...)=>
		ok, err = xpcall fn, self\log_traceback, self, ...
		if not ok
			Logger.debug "Arguments:"
			for arg in *{...}
				Logger.debug tostring arg
		return ok, err

	--- Call a function without self and, when failed, print debug information
	-- @tparam function fn Function to be called
	-- @param ... vararg list to pass to function
	pcall_bare: (fn, ...)=>
		ok, err = xpcall fn, self\log_traceback, ...
		if not ok
			Logger.debug "Arguments:"
			Logger.debug tostring arg for arg in *{...}
		return ok, err

	--- Print a traceback using the internal logging mechanism
	-- @see IRCClient\log
	log_traceback: (err)=>
		err = tostring err
		Logger.debug moonscript.errors.rewrite_traceback debug.traceback!, err
		Logger.debug "#{Logger.level.error} ---"
		return err

	--- 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, :priority}


quiz-state: checkbox with a clickable label


Sudden fix for MusicPlayer

Sudden fix for MusicPlayer

love = love

STATE_FADEIN = 1
STATE_NORMAL = 2
STATE_FADEOUT = 3
STATE_FINAL = 4

class Track
  new: (source) =>
    assert(type(source) == "string")
    @source = love.audio.newSource(source, "stream")
    @started = false

  set: (args) =>
    for k, v in pairs args
      @[k] = v
    return@

  play: (fadein) =>
    fadein = fadein or @fadein
    @started = true
    if not fadein or fadein == 0
      @level = 1
      @state = STATE_NORMAL
    else
      @level = 0
      @state = STATE_FADEIN
      @fadein_speed = 1 / fadein
    @source\setVolume(@level)
    @source\play()

  resume: (fadein) =>
    fadein = fadein or @fadein
    if not fadein or fadein == 0
      @level = 1
      @state = STATE_NORMAL
    else
      @level = @level or 0
      @state = STATE_FADEIN
      @fadein_speed = 1 / fadein
    @source\setVolume(@level)
    if not @started
      @source\play!
    else
      @source\resume!

  stop: (fadeout) =>
    fadeout = fadeout or @fadeout
    @started = false
    if not fadeout or fadeout == 0
      @state = STATE_FINAL
      @level = 0
    else
      @state = STATE_FADEOUT
      @fadeout_speed = 1 / fadeout
      @action = @source.stop

  pause: (fadeout) =>
    fadeout = fadeout or @fadeout
    if not fadeout or fadeout == 0
      @state = STATE_FINAL
      @level = 0
    else
      @state = STATE_FADEOUT
      @fadeout_speed = 1 / fadeout
      @action = @source.pause

  update: (dt) =>
    if @state == STATE_FADEIN
      @level += dt * @fadein_speed
    elseif @state == STATE_FADEOUT
      @level -= dt * @fadeout_speed
    if @level < 0
      @level = 0
      @.action(@source)
    elseif @level > 1
      @level = 1
    @source\setVolume(@level)

local MusicPlayer
MusicPlayer =
  tracks: {}
  aliases: {}
  tags: {}
  stack: {}
  playTrack: (args) ->
    assert args.name and args.alias
    track = Track("resources/music/"..args.name..".ogg")\set{fadein:1, fadeout:1}
    table.insert(MusicPlayer.stack, track)
    MusicPlayer.aliases[args.alias] = track
    track\play!
    return MusicPlayer

  sendEvent: (args) ->
    for k, track in pairs MusicPlayer.tracks
      track[args.event] track, args.parameters

  sendEventToTag: (args) ->
    assert(args.event and args.tag)
    if not MusicPlayer.tags[args.tag]
      return
    for i, idx in ipairs MusicPlayer.tags[args.tag]
      MusicPlayer.tracks[idx][args.event] MusicPlayer.tracks[idx], args.parameters

  sendEventToAlias: (args) ->
    assert(args.event and args.alias)
    MusicPlayer.tracks[MusicPlayer.aliases[args.alias]][args.event] args.parameters

  getTrack: (alias) ->
    return MusicPlayer.tracks[MusicPlayer.aliases[alias]]

  update: (dt) ->
    for _, track in pairs MusicPlayer.stack
      track\update dt
    return MusicPlayer

  pauseCurrent: ->
    if MusicPlayer.stack[#MusicPlayer.stack]
      MusicPlayer.stack[#MusicPlayer.stack]\pause!

  resumeCurrent: ->
    if MusicPlayer.stack[#MusicPlayer.stack]
      MusicPlayer.stack[#MusicPlayer.stack]\resume!

return MusicPlayer

love = love

STATE_FADEIN = 1
STATE_NORMAL = 2
STATE_FADEOUT = 3
STATE_FINAL = 4

class Track
  new: (source) =>
    assert(type(source) == "string")
    @source = love.audio.newSource(source)
    @started = false

  set: (args) =>
    for k, v in pairs args
      @[k] = v
    return@

  play: (fadein) =>
    fadein = fadein or @fadein
    @started = true
    if not fadein or fadein == 0
      @level = 1
      @state = STATE_NORMAL
    else
      @level = 0
      @state = STATE_FADEIN
      @fadein_speed = 1 / fadein
    @source\setVolume(@level)
    @source\play()

  resume: (fadein) =>
    fadein = fadein or @fadein
    if not fadein or fadein == 0
      @level = 1
      @state = STATE_NORMAL
    else
      @level = @level or 0
      @state = STATE_FADEIN
      @fadein_speed = 1 / fadein
    @source\setVolume(@level)
    if not @started
      @source\play!
    else
      @source\resume!

  stop: (fadeout) =>
    fadeout = fadeout or @fadeout
    @started = false
    if not fadeout or fadeout == 0
      @state = STATE_FINAL
      @level = 0
    else
      @state = STATE_FADEOUT
      @fadeout_speed = 1 / fadeout
      @action = @source.stop

  pause: (fadeout) =>
    fadeout = fadeout or @fadeout
    if not fadeout or fadeout == 0
      @state = STATE_FINAL
      @level = 0
    else
      @state = STATE_FADEOUT
      @fadeout_speed = 1 / fadeout
      @action = @source.pause

  update: (dt) =>
    if @state == STATE_FADEIN
      @level += dt * @fadein_speed
    elseif @state == STATE_FADEOUT
      @level -= dt * @fadeout_speed
    if @level < 0
      @level = 0
      @.action(@source)
    elseif @level > 1
      @level = 1
    @source\setVolume(@level)

local MusicPlayer
MusicPlayer =
  tracks: {}
  aliases: {}
  tags: {}
  stack: {}
  playTrack: (args) ->
    assert args.name and args.alias
    track = Track("resources/music/"..args.name..".ogg")\set{fadein:1, fadeout:1}
    table.insert(MusicPlayer.stack, track)
    MusicPlayer.aliases[args.alias] = track
    track\play!
    return MusicPlayer

  sendEvent: (args) ->
    for k, track in pairs MusicPlayer.tracks
      track[args.event] track, args.parameters

  sendEventToTag: (args) ->
    assert(args.event and args.tag)
    if not MusicPlayer.tags[args.tag]
      return
    for i, idx in ipairs MusicPlayer.tags[args.tag]
      MusicPlayer.tracks[idx][args.event] MusicPlayer.tracks[idx], args.parameters

  sendEventToAlias: (args) ->
    assert(args.event and args.alias)
    MusicPlayer.tracks[MusicPlayer.aliases[args.alias]][args.event] args.parameters

  getTrack: (alias) ->
    return MusicPlayer.tracks[MusicPlayer.aliases[alias]]

  update: (dt) ->
    for _, track in pairs MusicPlayer.stack
      track\update dt
    return MusicPlayer

  pauseCurrent: ->
    if MusicPlayer.stack[#MusicPlayer.stack]
      MusicPlayer.stack[#MusicPlayer.stack]\pause!

  resumeCurrent: ->
    if MusicPlayer.stack[#MusicPlayer.stack]
      MusicPlayer.stack[#MusicPlayer.stack]\resume!

return MusicPlayer


lib/models/user: away status is string, add realname, add is_self

import Map, Option from require 'lib.std'

class User
	new: (bot, nick, user, host, opts={})=>
		@_bot = assert bot
		@nick = assert nick
		@user = assert user
		@host = assert host
		@account = Option opts.account
		@away = Option opts.away
		@realname = Option opts.realname
		@is_self = opts.is_self or false
		@channels = Map opts.channels or {}

	--- Return whether or not the client is visible to the bot
	-- @treturn boolean
	is_visible: =>
		-- Add more as the bot "follows" more
		-- This is the bot itself, therefore it can always see itself
		return true if @is_self

		-- This is the amount of channels the bot can see the user in
		return true if #@channels == 0

		-- The bot can't see the user in any other fashion
		false

return :User

import Map, Option from require 'lib.std'

class User
	new: (bot, nick, user, host, opts={})=>
		@_bot = assert bot
		@nick = assert nick
		@user = assert user
		@host = assert host
		@account = Option opts.account
		@away = opts.away == true
		@channels = Map opts.channels or {}

	is_visible: =>
		-- Add more as the bot "follows" more
		return true if #@channels == 0
		false

return :User


trigger: improve list. lay groundwork for future destination source

Former-commit-id: 905dddc24d4d28b67c70ce96644486d1fb8890a6 [formerly 2c6785630c6438aff6fe3693756a1ad7640c9240]
Former-commit-id: 8c24605c4853c0569542bdb6d943b055d2c04ecb

sql = require'lsqlite3'

openDb = ->
  db = sql.open 'cache/trigger.sql'

  db\exec [[
    CREATE TABLE IF NOT EXISTS trigger (
      name UNIQUE ON CONFLICT REPLACE,
      pattern UNIQUE ON CONFLICT REPLACE,
      destination,
      funcstr
    );
    ]]
  return db

triggerHelp  = (source, destination, argument) =>
  help = 'Usage: ¤trigger add <name>|<lua pattern>|<lua code>. Ex: ¤trigger add help|^!help (%w+)|say "%s, you need help. Delete: ¤trigger del <name>. List: ¤trigger list. Show: ¤trigger show <name>"'
  patt = @ChannelCommandPattern('^%p', 'triger', destination)
  help = help\gsub '¤', patt
  @Msg 'privmsg', destination, source, help

-- Construct a safe environ for trigger with two functions; say and reply
--- Preferrably you should be able to cross-call modules here to make aliasing possible
-- TODO: needs a proper sandbox to prevent endless loops, etc
sandbox = (func) ->
  (source, destination, ...) =>
    -- TODO check destination here to decide if it's a local or global trigger
    -- Store args so we can use them in env
    arg = {...}
    env =
      say: (str) ->
        @Msg 'privmsg', destination, source, str, unpack(arg),
      reply: (str) ->
        @Msg 'privmsg', destination, source, source.nick..': '..str, unpack(arg),
      print: (str) ->
        @Msg 'privmsg', destination, source, str, unpack(arg),
      :source
      :destination
      simplehttp:require'simplehttp'
      json:require'json'
      :string
      :type
      :tostring
      :tonumber
      :ipars
      :pairs
      :table
      :next

    --setfenv func, setmetatable(env, {__index: _G })
    setfenv func, env
    success, err = pcall func, arg
    if err
      @Msg 'privmsg', destination, source, err
    else
      success

-- Construct a safe function to run and return a handler
triggerHandler = (source, destination, funcstr) =>
  func, err = loadstring funcstr
  unless func
    @Msg 'privmsg', destination, source, 'Trigger error: %s', err
  else
    sandbox(func)

-- Register the command
regCommand = (source, destination, name, pattern, funcstr) =>
  db = openDb!
  ins = db\prepare "INSERT INTO trigger (name, pattern, funcstr, destination) VALUES(?, ?, ?, ?)"
  code = ins\bind_values name, pattern, funcstr, destination
  code = ins\step!
  code = ins\finalize!
  db\close!

  @RegisterCommand 'trigger', pattern, triggerHandler(@, source, destination, funcstr)

delCommand = (source, destination, name) =>
  db = openDb!
  -- Find pattern which is used for deletion of handler
  stmt = db\prepare "SELECT pattern FROM trigger WHERE name = ?"
  code = stmt\bind_values name
  code = stmt\step!
  if code == sqlite3.DONE
    -- Invalid name
    db\close!
    @Msg 'privmsg', destination, source, 'Invalid name'
    return
  pattern = stmt\get_values()[1]

  -- Delete trigger from db
  ins = db\prepare "DELETE FROM trigger WHERE name = ?"
  code = ins\bind_values name
  code = ins\step!
  code = ins\finalize!
  db\close!

  @UnregisterCommand 'triggers', pattern
  @Msg 'privmsg', destination, source, "Trigger #{pattern} deleted"

listTriggers = (source, destination) =>
  db = openDb!
  stmt = db\prepare "SELECT name FROM trigger"
  out = {}
  for row in stmt\nrows! do
    table.insert out, row.name

  db\close!

  if #out > 0
    @Msg 'privmsg', destination, source, "Triggers: "..table.concat(out, ', ')
  else
    @Msg 'privmsg', destination, source, "No triggers defined"

showTrigger = (source, destination, name) =>
  db = openDb!
  stmt = db\prepare "SELECT * FROM trigger WHERE name = ?"
  code = stmt\bind_values name
  for row in stmt\nrows! do
    @Msg 'privmsg', destination, source, "Trigger: \002#{row.name}\002, pattern: #{row.pattern}, code: #{row.funcstr}"
  db\close!

-- Register commands on startup
db = openDb!
for row in db\nrows 'SELECT pattern, funcstr, destination FROM trigger'
  ivar2\RegisterCommand 'triggers', row.pattern, triggerHandler(ivar2, nil, row.destination, row.funcstr)
db\close!

PRIVMSG:
  '^%ptrigger$': triggerHelp
  '^%ptrigger add (.+)|(.+)|(.+)$': regCommand
  '^%ptrigger del (.+)$': delCommand
  '^%ptrigger list$': listTriggers
  '^%ptrigger show (.+)$': showTrigger

sql = require'lsqlite3'

openDb = ->
  db = sql.open 'cache/trigger.sql'

  db\exec [[
    CREATE TABLE IF NOT EXISTS trigger (
      name UNIQUE ON CONFLICT REPLACE,
      pattern UNIQUE ON CONFLICT REPLACE,
      funcstr
    );
    ]]
  return db

triggerHelp  = (source, destination, argument) =>
   @Msg 'privmsg', destination, source, 'Usage: !trigger add <name>|<lua pattern>|<lua code>. Ex: !trigger add help|^!help (%w+)|say "%s, you need help. Delete: !trigger del <name>"'

-- Construct a safe environ for trigger with two functions; say and reply
--- Preferrably you should be able to cross-call modules here to make aliasing possible
sandbox = (func) ->
  (source, destination, ...) =>
    -- Store args so we can use them in env
    arg = {...}
    env =
      say: (str) ->
        @Msg 'privmsg', destination, source, str, unpack(arg),
      reply: (str) ->
        @Msg 'privmsg', destination, source, source.nick..': '..str, unpack(arg),
      print: (str) ->
        @Msg 'privmsg', destination, source, str, unpack(arg),
      simplehttp:require'simplehttp'
      json:require'json'
      :string
      :type
      :tostring
      :tonumber
      :ipars
      :pairs
      :table
      :next

    --setfenv func, setmetatable(env, {__index: _G })
    setfenv func, env
    success, err = pcall func, arg
    if err
      @Msg 'privmsg', destination, source, err
    else
      success

-- Construct a safe function to run and return a handler
triggerHandler = (source, destination, funcstr) =>
  func, err = loadstring funcstr
  unless func
    @Msg 'privmsg', destination, source, 'Trigger error: %s', err
  else
    sandbox(func)

-- Register the command
regCommand = (source, destination, name, pattern, funcstr) =>
  db = openDb!
  ins = db\prepare "INSERT INTO trigger (name, pattern, funcstr) VALUES(?, ?, ?)"
  code = ins\bind_values name, pattern, funcstr
  code = ins\step!
  code = ins\finalize!
  db\close!

  @RegisterCommand 'trigger', pattern, triggerHandler(@, source, destination, funcstr)

delCommand = (source, destination, name) =>
  db = openDb!
  -- Find pattern which is used for deletion of handler
  stmt = db\prepare "SELECT pattern FROM trigger WHERE name = ?"
  code = stmt\bind_values name
  code = stmt\step!
  if code == sqlite3.DONE
    -- Invalid name
    db\close!
    @Msg 'privmsg', destination, source, 'Invalid name'
    return
  pattern = stmt\get_values()[1]

  -- Delete trigger from db
  ins = db\prepare "DELETE FROM trigger WHERE name = ?"
  code = ins\bind_values name
  code = ins\step!
  code = ins\finalize!
  db\close!

  @UnregisterCommand 'triggers', pattern
  @Msg 'privmsg', destination, source, "Trigger #{pattern} deleted"

listTriggers = (source, destination) =>
  db = openDb!
  stmt = db\prepare "SELECT name FROM trigger"
  out = {}
  for row in stmt\nrows! do
    table.insert out, row.name

  db\close!

  if #out > 0
    @Msg 'privmsg', destination, source, "Triggers: "..table.concat(out, ', ')

showTrigger = (source, destination, name) =>
  db = openDb!
  stmt = db\prepare "SELECT * FROM trigger WHERE name = ?"
  code = stmt\bind_values name
  for row in stmt\nrows! do
    @Msg 'privmsg', destination, source, "Trigger: \002#{row.name}\002, pattern: #{row.pattern}, code: #{row.funcstr}"
  db\close!

-- Register commands on startup
db = openDb!
for row in db\nrows 'SELECT pattern, funcstr FROM trigger'
  ivar2\RegisterCommand 'triggers', row.pattern, triggerHandler(ivar2, nil, nil, row.funcstr)
db\close!

PRIVMSG:
  '^%ptrigger$': triggerHelp
  '^%ptrigger add (.+)|(.+)|(.+)$': regCommand
  '^%ptrigger del (.+)$': delCommand
  '^%ptrigger list$': listTriggers
  '^%ptrigger show (.+)$': showTrigger


plugins/command: Fix issue

plugins/command: Fix issue

import IRCClient from require "irc"
import sleep from require "cqueues"

unpack = unpack or table.unpack

handle_error = (channel, command)=> (err)->
	@log_traceback(err)
	@send "COMMAND_ERR", channel, command, "Error: #{err}"

IRCClient\add_sender 'COMMAND_OK', (target, command_name, message)=>
	@send "PRIVMSG", target, "[\00303#{command_name}\003] #{message}"

IRCClient\add_sender 'COMMAND_ERR', (target, command_name, message)=>
	@send "PRIVMSG", target, "[\00304!#{command_name}\003] #{message}"

IRCClient\add_handler 'PRIVMSG', (prefix, args)=>
	{channel, message} = args

	cmd_prefix = @config.prefix
	return unless message\sub(1, #cmd_prefix) == cmd_prefix

	cmd_name = message\match "%S+", #cmd_prefix + 1
	command_args = {prefix, channel}
	command = @commands\get cmd_name, multiple: false, index: IRCClient.commands
	return @send "COMMAND_ERR", channel, "core", "Command not found: #{cmd_name}" unless command

	args = [arg for arg in message\gmatch "%S+", (message\find("%s") or #message) + 1]
	command_args[#command_args + 1] = table.concat(args, " ")
	command_args[#command_args + 1] = arg for arg in *args
	xpcall command, handle_error(self, channel, cmd_name), self, unpack(command_args)

IRCClient\add_command "test", async: true, (prefix, channel)=>
	{:nick} = prefix

	sleep 5

	if @users[nick] and @users[nick].account
		@send "COMMAND_OK", channel, "test", "Account name: #{@users[nick].account}"
	else
		@send "COMMAND_ERR", channel, "test", "Account not found for: #{nick}"

import IRCClient from require "irc"
import sleep from require "cqueues"

unpack = unpack or table.unpack

handle_error = (channel, command)=> (err)->
	@log_traceback(err)
	@send "COMMAND_ERR", channel, command, "Error: #{err}"

IRCClient\add_sender 'COMMAND_OK', (target, command_name, message)=>
	@send "PRIVMSG", target, "[\00303#{command_name}\003] #{message}"

IRCClient\add_sender 'COMMAND_ERR', (target, command_name, message)=>
	@send "PRIVMSG", target, "[\00304!#{command_name}\003] #{message}"

IRCClient\add_handler 'PRIVMSG', (prefix, args)=>
	{channel, message} = args

	cmd_prefix = @config.prefix
	return unless message\sub(1, #cmd_prefix) == cmd_prefix

	cmd_name = message\match "%S+", #cmd_prefix + 1
	command_args = {prefix, channel}
	command = @commands\get cmd_name, multiple: false, index: IRCClient.commands
	return @send "COMMAND_ERR", channel, "core", "Command not found: #{cmd_name}" unless command

	args = [arg for arg in message\gmatch "%S+", (message\find("%s") or #message) + 1]
	command_args[#command_args + 1] = table.concat(args, " ")
	command_args[#command_args + 1] = arg for arg in *args
	xpcall command, handle_error(self, channel, cmd_name), self, unpack(command_args)

IRCClient\add_command "test", async: true, (prefix, channel)=>
	nick = prefix\match "^[^!]+"

	sleep 5

	if @users[nick] and @users[nick].account
		@send "COMMAND_OK", channel, "test", "Account name: #{@users[nick].account}"
	else
		@send "COMMAND_ERR", channel, "test", "Account not found for: #{nick}"


wc2014: fix multiple current matches

wc2014: fix multiple current matches

Former-commit-id: 46646178e7283f68ae05f529bcfcd002993497a5 [formerly f75220968610d7f429326146c77d6394b81cd5e9]
Former-commit-id: 2c30cc0f322c6a5251e684202b46113871a0c558

URL = 'http://worldcup.sfg.io/matches/'

bold = ivar2.util.bold

formatMatch = (m) ->
  out = "#{m.home_team.country} #{bold m.home_team.goals} - #{bold m.away_team.goals} #{m.away_team.country}"
  if m.status == 'in progress'
    out ..= " " .. ivar2.util.underline "In progress!"
  return out

PRIVMSG:
  '^%pwc today$': =>
    ivar2.util.simplehttp URL..'today', (json) ->
      data = ivar2.util.json.decode(json)
      out = {}
      for _,m in pairs(data)
        out[#out+1] = "["..formatMatch(m).."]"
      say table.concat(out, ' ')
  '^%pwc tomorrow$': =>
    ivar2.util.simplehttp URL..'tomorrow', (json) ->
      data = ivar2.util.json.decode(json)
      out = {}
      for _,m in pairs(data)
        out[#out+1] = "["..formatMatch(m).."]"
      say table.concat(out, ' ')
  '^%pwc$': =>
    ivar2.util.simplehttp URL..'current', (json) ->
      data = ivar2.util.json.decode(json)
      out = {}
      for _,m in pairs(data)
        out[#out+1] = "[#{m.home_team.code} #{bold m.home_team.goals} - #{bold m.away_team.goals} #{m.away_team.code}]"
      if #out < 1
        return reply "No match being played."
      say table.concat(out, ' ')

URL = 'http://worldcup.sfg.io/matches/'

bold = ivar2.util.bold

formatMatch = (m) ->
  out = "#{m.home_team.country} #{bold m.home_team.goals} - #{bold m.away_team.goals} #{m.away_team.country}"
  if m.status == 'in progress'
    out ..= " " .. ivar2.util.underline "In progress!"
  return out

PRIVMSG:
  '^%pwc today$': =>
    ivar2.util.simplehttp URL..'today', (json) ->
      data = ivar2.util.json.decode(json)
      out = {}
      for _,m in pairs(data)
        out[#out+1] = "["..formatMatch(m).."]"
      say table.concat(out, ' ')
  '^%pwc tomorrow$': =>
    ivar2.util.simplehttp URL..'tomorrow', (json) ->
      data = ivar2.util.json.decode(json)
      out = {}
      for _,m in pairs(data)
        out[#out+1] = "["..formatMatch(m).."]"
      say table.concat(out, ' ')
  '^%pwc$': =>
    ivar2.util.simplehttp URL..'current', (json) ->
      data = ivar2.util.json.decode(json)
      m = data[1]
      unless m
        return reply "No match being played."
      else
        say "#{m.home_team.code} #{bold m.home_team.goals} - #{bold m.away_team.goals} #{m.away_team.code}"