From 75d3f149f2000b51c0729cd2e5690f58222c1ed7 Mon Sep 17 00:00:00 2001 From: mio Date: Sun, 20 Mar 2022 03:42:25 +0000 Subject: [PATCH] Add handlers to list servers/channels, connect to/quit servers - Add handlers to list connected servers and channels - Add handlers to connect and disconnect from servers - Check user config and only override specified settings - Redact parameters in admin-only handlers from debug output to avoid logging potential passwords - Set admin-only handlers to respond only in private messages - Include list of admin handlers for customisation - Extract debug strings to a table for customisation --- examples/ramenkan.config.lua | 1 - itte.lua | 493 ++++++++++++++++++++++++----------- itteutil.lua | 44 +++- 3 files changed, 383 insertions(+), 155 deletions(-) diff --git a/examples/ramenkan.config.lua b/examples/ramenkan.config.lua index 8764cce..7092baa 100644 --- a/examples/ramenkan.config.lua +++ b/examples/ramenkan.config.lua @@ -381,7 +381,6 @@ end -- ]] itte_config = { debug = true, - notify_errors = true, messages = { help = "一、二、三、らーめん缶! Hello, I am a ramen vending machine. " .. "Please type a code for service: {{codes}} " .. diff --git a/itte.lua b/itte.lua index c5223cb..6eb30a6 100644 --- a/itte.lua +++ b/itte.lua @@ -23,22 +23,40 @@ itte.confs = { -- Default application settings itte.config = { debug = false, - notify_errors = false, + admin_handlers = { "connect", "quit", "reload", "servers", "join", "part" }, messages = { + connect_success = "Connected to {{server}}.", help = "Service codes available: {{codes}}", join = "We're in.", + list_all_servers = "Known servers: {{servers}}", + list_channels = "Channels: {{channels}}", + list_conn_servers = "Connected servers: {{servers}}", part = "The bot is greater than the sum of its parts.", ping = "Pong!", quit = "Goodbye.", + quit_success = "Quit {{server}}.", reload = "Splines recticulated.", }, errors = { + invalid_name = "Error: invalid server name `{{server}}`.", no_perm = "I'm sorry, {{user}}. I'm afraid I can't do that.", - notify_no_perm = "{{user}} attempted to use service code: {{code}}", ns_auth_failed = "Error: Nickserv identification failed.", sasl_auth_failed = "Error: SASL authentication failed.", unknown_code = "I don't understand what you just said.", }, + debugs = { + cap = { "auth_cap" }, + conn_svr = { "conn", "Connecting to {{server}} ..." }, + dc_svr = { "conn", "Connection to {{server}} closed." }, + exit = { "conn", "Exiting ..." }, + listen = { "listen" }, + nickserv = { "auth_nickserv" }, + privmsg = { "sender", "recipient", "reply_to", "code", "code_params", + "body" }, + redact = { "********" }, + send = { "send" }, + svrs_not_found = { "config", "Error: servers not found." }, + }, } -- Store server info @@ -79,7 +97,7 @@ function itte.help(name) end -itte.docs.get_config = [[ () +itte.docs.get_config = [[ ([reload_bool]) Load the config file. ]] function itte.get_config(reload) @@ -101,23 +119,29 @@ function itte.get_config(reload) if itte.servers ~= nil then itte.servers = itte_servers else - util.debug("config", "Error: servers not found.", itte.config.debug) + util.debug(itte.config.debugs.svrs_not_found[1], + itte.config.debugs.svrs_not_found[2], itte.config.debug) do return end end - if itte_config ~= nil then itte.config = itte_config end + -- Update config with value overrides from itte_config + if itte_config ~= nil then + for k, v in pairs(itte_config) do + itte.config[k] = v + end + end if itte_handlers ~= nil then itte.handlers = itte_handlers end if itte_admins ~= nil then itte.admins = itte_admins end -- If reloading, reconstruct the context tables. - -- This only works if the instance name has not changed between reloads. + -- This only works if the name name has not changed between reloads. if reload then - for instance, prop in pairs(itte.servers) do - cxt_old = itte.contexts[instance] - itte.contexts[instance] = prop - itte.contexts[instance].name = instance - itte.contexts[instance].cmds = itte.get_commands(prop) - itte.contexts[instance].con = cxt_old.con - itte.contexts[instance].con:settimeout(0.5) + for name, prop in pairs(itte.servers) do + cxt_old = itte.contexts[name] + itte.contexts[name] = prop + itte.contexts[name].name = name + itte.contexts[name].cmds = itte.get_commands(prop) + itte.contexts[name].con = cxt_old.con + itte.contexts[name].con:settimeout(0.5) end end end @@ -211,12 +235,18 @@ function itte.get_commands(svr) end -itte.docs.send_command = [[ (context_table, cmd_str) +itte.docs.send_command = [[ (socket_obj, command_str [, redact_bool]) Format a IRC command string to send through a socket connection. + If `redact` is set to true, the debug output contents will be redacted. ]] -function itte.send_command(con, str) +function itte.send_command(con, str, redact) con:send(str .. "\r\n") - util.debug("send", str, itte.config.debug) + if redact == true then + util.debug(itte.config.debugs.send[1], itte.config.debugs.redact[1], + itte.config.debug) + else + util.debug(itte.config.debugs.send[1], str, itte.config.debug) + end end @@ -231,10 +261,10 @@ function itte.message(cxt, users, str) end -itte.docs.connect_server = [[ (name_str, server_table) +itte.docs.init_socket = [[ (name_str, server_table) Initialise a socket connection to a server and return a context table. ]] -function itte.connect_server(name, svr) +function itte.init_socket(name, svr) -- Load server context local context = svr context.name = name @@ -259,8 +289,9 @@ function itte.connect_server(name, svr) options = "all", } - util.debug("conn", "Connecting to " .. svr.host .. "/" .. svr.port .. - " ...", itte.config.debug) + util.debug(itte.config.debugs.conn_svr[1], + string.gsub(itte.config.debugs.conn_svr[2], + "{{server}}", svr.host .. "/" .. svr.port), itte.config.debug) context.con:connect(svr.host, svr.port) context.con = ssl.wrap(context.con, con_params) context.con:dohandshake() @@ -275,12 +306,63 @@ function itte.connect_server(name, svr) end +itte.docs.connect_server = [[ (name_str) + Connect to a server and join the channel(s) specified in the config. + ]] +function itte.connect_server(name) + itte.contexts[name] = itte.init_socket(name, itte.servers[name]) + + -- For PASS-based authentication, send PASS before greeting the server + if itte.contexts[name].auth_type == "pass" then + itte.send_command(itte.contexts[name].con, + itte.contexts[name].cmds.pass.resp, true) + itte.contexts[name].state.authed = true + end + + -- Greet the server in all auth scenarios except SASL, which greets the + -- server during CAP negotiation. + if (itte.contexts[name].auth_type ~= "sasl") then + itte.send_command(itte.contexts[name].con, + itte.contexts[name].cmds.user.resp) + itte.send_command(itte.contexts[name].con, + itte.contexts[name].cmds.nick.resp) + end + + local joined_chans = false + while (itte.contexts[name].state.connected) and (not joined_chans) do + local data, status = itte.contexts[name].con:receive() + + if itte.contexts[name].auth_type == "sasl" then + itte.negotiate_cap(itte.contexts[name], data) + + elseif itte.contexts[name].auth_type == "nickserv" then + itte.auth_nickserv(itte.contexts[name], data) + end + + -- Minimum requirements for joining channels: client has authenticated if + -- an auth type is set, or has received a NOTICE [nick] message during an + -- ident lookup. + if itte.contexts[name].state.authed then + itte.traverse_channels(itte.contexts[name], "join") + joined_chans = true + elseif (data ~= nil) then + if util.is_substr(data, itte.contexts[name].cmds.join.check) + then + itte.traverse_channels(itte.contexts[name], "join") + joined_chans = true + end + end + end +end + + itte.docs.disconnect_server = [[ (name_str) Close a socket connection to a server. ]] function itte.disconnect_server(name) if itte.contexts[name] ~= nil then - util.debug("conn", "Connection to " .. name .. " closed.", + util.debug(itte.config.debugs.dc_svr[1], + string.gsub(itte.config.debugs.dc_svr[2], "{{server}}", name), itte.config.debug) end itte.contexts[name].con:close() @@ -289,7 +371,8 @@ function itte.disconnect_server(name) -- Check if it is the last connection and trigger client exit -- if no connections remain if #util.table_keys(itte.contexts) == 0 then - util.debug("conn", "Exiting ...", itte.config.debug) + util.debug(itte.config.debugs.exit[1], itte.config.debugs.exit[2], + itte.config.debug) itte.state.connected = false end end @@ -307,7 +390,7 @@ function itte.negotiate_cap(cxt, str) end if (str ~= nil) then - util.debug("auth", str, itte.config.debug) + util.debug(itte.config.debugs.cap[1], str, itte.config.debug) -- Wait for server to respond with capabilities list before greeting if (not cxt.state.cap_greeted) then @@ -329,7 +412,7 @@ function itte.negotiate_cap(cxt, str) -- Format of the string to encode: "user\0user\0password" local sasl_pass, _ = mime.b64(cxt.auth_user .. "\0" .. cxt.auth_user .. "\0" .. cxt.auth_pass) - itte.send_command(cxt.con, cxt.cmds.auth_pass.resp .. sasl_pass) + itte.send_command(cxt.con, cxt.cmds.auth_pass.resp .. sasl_pass, true) -- Look for auth success signal and end cap negotiation elseif (util.is_substr(str, cxt.cmds.cap_end.check)) then @@ -353,10 +436,10 @@ itte.docs.auth_nickserv = [[ (context_table, data_str) ]] function itte.auth_nickserv(cxt, str) if str ~= nil then - util.debug("auth", str, itte.config.debug) + util.debug(itte.config.debugs.nickserv[1], str, itte.config.debug) if util.is_substr(str, cxt.cmds.ns_identify.check) then - itte.send_command(cxt.con, cxt.cmds.ns_identify.resp) + itte.send_command(cxt.con, cxt.cmds.ns_identify.resp, true) cxt.state.ns_checked = true elseif util.is_substr(str, cxt.cmds.ns_identify_pass.check) then cxt.state.authed = true @@ -370,9 +453,22 @@ function itte.auth_nickserv(cxt, str) end -itte.docs.is_admin = [[ (admins_table, message_table, mode_str) - Check whether a user is an admin. Modes: global, instance. If `mode` is - unspecified, check for the user in the instance's admins table. Return true +itte.docs.is_pm = [[ (message_table) + Check whether a message is a private message. Return true if it is a private + message or false otherwise. + ]] +function itte.is_pm(msg) + if util.is_substr(msg.reply_to, "#") then + return false + else + return true + end +end + + +itte.docs.is_admin = [[ (admins_table, message_table [, mode_str]) + Check whether a user is an admin. Modes: global, name. If `mode` is + unspecified, check for the user in the name's admins table. Return true if the user and password are in the given admin table, or false otherwise. ]] function itte.is_admin(admins, msg, mode) @@ -380,12 +476,15 @@ function itte.is_admin(admins, msg, mode) -- If no admin users set or no password provided if (admins == nil) or (table.concat(msg.code_params) == "") then do return false end - -- Incorrect password provided + elseif msg.code_params ~= nil then - if mode == "global" then - -- For global admins, both user and password are supplied in the message - -- to keep the auth network-independent - in_admins = util.is_entry(admins, msg.code_params[1], + -- No password provided in global mode + if (mode == "global") and (#util.table_keys(msg.code_params) < 2) then + do return false end + elseif (mode == "global") then + -- For global admins, both user and password are supplied as the last two + -- values in the message parameters to keep the auth network-independent. + in_admins = util.is_entry(admins, msg.code_params[#msg.code_params - 1], msg.code_params[#msg.code_params]) else -- User is the IRC user, password is suppled in the message @@ -393,6 +492,7 @@ function itte.is_admin(admins, msg, mode) msg.code_params[#msg.code_params]) end + -- Incorrect password provided if not in_admins then do return false end else @@ -402,18 +502,25 @@ function itte.is_admin(admins, msg, mode) end -itte.docs.notify_no_perms = [[ (context_table, message_table) - Respond to an attempt to use service code by a non-admin user and send a - message to the admins if `notify_errors` is enabled in the config. +itte.docs.is_allowed = [[ (context_table, message_table [, mode_str]) + Check if user is allowed to run a handler. If the handler is for admins only, + check whether the request was sent in a private message and the user is an + admin (authorised). Set `mode` to "global" to check for global admins. + Returns true if the user is allowed or false otherwise. ]] -function itte.notify_no_perms(cxt, msg) - itte.message(cxt, { msg.reply_to }, string.gsub(itte.config.errors.no_perm, - "{{user}}", msg.sender)) - if (itte.config.notify_errors) and (cxt.admins ~= nil) then - local notify_msg = string.gsub(itte.config.errors.notify_no_perm, - "{{user}}", msg.sender) - notify_msg = string.gsub(notify_msg, "{{code}}", msg.code) - itte.message(cxt, util.table_keys(cxt.admins), notify_msg) +function itte.is_allowed(cxt, msg, mode) + if not util.has_key(itte.config.admin_handlers, msg.code) then + return true + else + local admins = cxt.admins + if mode == "global" then admins = itte.admins end + if (itte.is_pm(msg)) and itte.is_admin(admins, msg, mode) then + return true + else + itte.message(cxt, { msg.reply_to }, + string.gsub(itte.config.errors.no_perm, "{{user}}", msg.sender)) + return false + end end end @@ -430,10 +537,22 @@ function itte.traverse_channels(cxt, mode, chans) if mode == "join" then itte.send_command(cxt.con, cxt.cmds.join.resp .. table.concat(channels, ",")) + -- Append to the channels table + for c = 1, #channels do + if not util.has_key(cxt.channels, channels[c]) then + table.insert(cxt.channels, channels[c]) + end + end elseif mode == "part" then itte.send_command(cxt.con, cxt.cmds.part.resp .. table.concat(channels, ",")) + -- Remove from the channels table + for c = 1, #channels do + if util.has_key(cxt.channels, channels[c]) then + table.remove(cxt.channels, util.find_key(cxt.channels, channels[c])) + end + end end end @@ -475,13 +594,20 @@ function itte.parse_privmsg(cxt, str) msg.reply_to = msg.recipient end + -- Redact debug output for admin-only handlers to avoid logging passwords. + local body = msg.body + local code_params = table.concat(msg.code_params, " ") + if util.has_key(itte.config.admin_handlers, msg.code) then + body = itte.config.debugs.redact[1] + code_params = itte.config.debugs.redact[1] + end util.debug("privmsg", "{ " .. - "sender: " .. msg.sender .. - ", recipient: " .. msg.recipient .. - ", reply_to: " .. msg.reply_to .. - ", code: " .. msg.code .. - ", code_params: " .. table.concat(msg.code_params, " ") .. - ", body: " .. msg.body .. + itte.config.debugs.privmsg[1] .. ": " .. msg.sender .. + ", " .. itte.config.debugs.privmsg[2] .. ": " .. msg.recipient .. + ", " .. itte.config.debugs.privmsg[3] .. ": " .. msg.reply_to .. + ", " .. itte.config.debugs.privmsg[4] .. ": " .. msg.code .. + ", " .. itte.config.debugs.privmsg[5] .. ": " .. code_params .. + ", " .. itte.config.debugs.privmsg[6] .. ": " .. body .. " }", itte.config.debug) return msg end @@ -491,11 +617,144 @@ end -- Service code handlers -- --------------------------------------------------------------------------- +itte.docs._h.connect = [[ (context_table, message_table) + Connect to a server. + ]] +function itte._h.connect(cxt, msg) + if not itte.is_allowed(cxt, msg, "global") then + do return end + end + + if #util.table_keys(msg.code_params) > 2 then + -- Look up name and connect to the server + for s = 1, #msg.code_params - 2 do + if util.has_key(itte.servers, msg.code_params[s]) then + itte.connect_server(msg.code_params[s]) + local succ_str = string.gsub(itte.config.messages.connect_success, + "{{server}}", msg.code_params[s]) + itte.message(cxt, { msg.reply_to }, succ_str) + else + local err_str = string.gsub(itte.config.errors.invalid_name, + "{{server}}", msg.code_params[s]) + itte.message(cxt, { msg.reply_to }, err_str) + end + end + else + local err_str = string.gsub(itte.config.errors.invalid_name, + "{{server}}", msg.code_params[1]) + itte.message(cxt, { msg.reply_to }, err_str) + end +end + + +itte.docs._h.quit = [[ (context_table, message_table) + Disconnect from a server or multiple servers. + ]] +function itte._h.quit(cxt, msg) + if not itte.is_allowed(cxt, msg, "global") then + do return end + end + + -- If no server is specified, quit the server name where the message + -- originated + if #util.table_keys(msg.code_params) == 2 then + itte.send_command(cxt.con, cxt.cmds.quit.resp .. itte.config.messages.quit) + else + -- Look up name and send quit message to the specified server(s) + for s = 1, #msg.code_params - 2 do + if util.has_key(itte.contexts, msg.code_params[s]) then + itte.send_command(itte.contexts[msg.code_params[s]].con, + itte.contexts[msg.code_params[s]].cmds.quit.resp .. + itte.config.messages.quit) + local succ_str = string.gsub(itte.config.messages.quit_success, + "{{server}}", msg.code_params[s]) + itte.message(cxt, { msg.reply_to }, succ_str) + else + local err_str = string.gsub(itte.config.errors.invalid_name, + "{{server}}", msg.code_params[s]) + itte.message(cxt, { msg.reply_to }, err_str) + end + end + end +end + + +itte.docs._h.reload = [[ (context_table, message_table) + Reload the server config. + ]] +function itte._h.reload(cxt, msg) + if not itte.is_allowed(cxt, msg, "global") then + do return end + end + + itte.get_config(true) + itte.message(cxt, { msg.reply_to }, itte.config.messages.reload) +end + + +itte.docs._h.servers = [[ (context_table, message_table) + List known servers. + ]] +function itte._h.servers(cxt, msg) + if not itte.is_allowed(cxt, msg, "global") then + do return end + end + + if itte.contexts ~= nil then + local conn_svrs_str = string.gsub(itte.config.messages.list_conn_servers, + "{{servers}}", table.concat(util.table_keys(itte.contexts), ", ")) + itte.message(cxt, { msg.reply_to }, conn_svrs_str) + end + if itte.servers ~= nil then + local all_svrs_str = string.gsub(itte.config.messages.list_all_servers, + "{{servers}}", table.concat(util.table_keys(itte.servers), ", ")) + itte.message(cxt, { msg.reply_to }, all_svrs_str) + end +end + + +itte.docs._h.join = [[ (context_table, message_table) + Join specified channels on the current server. + ]] +function itte._h.join(cxt, msg) + if not itte.is_allowed(cxt, msg) then + do return end + end + + if msg.code_params ~= {} then + -- Trim the last parameter, which is a password and not a channel + table.remove(msg.code_params) + itte.traverse_channels(cxt, "join", msg.code_params) + itte.message(cxt, { msg.reply_to }, itte.config.messages.join) + end +end + + +itte.docs._h.part = [[ (context_table, message_table) + Leave specified channels on the current server. + ]] +function itte._h.part(cxt, msg) + if not itte.is_allowed(cxt, msg) then + do return end + end + + if msg.code_params ~= {} then + -- Trim the last parameter, which is a password and not a channel + table.remove(msg.code_params) + itte.traverse_channels(cxt, "part", msg.code_params) + itte.message(cxt, { msg.reply_to }, itte.config.messages.part) + end +end + + itte.docs._h.help = [[ (context_table, message_table) Send a help message listing available service codes. ]] --- Send a help message listing available service codes. function itte._h.help(cxt, msg) + if not itte.is_allowed(cxt, msg) then + do return end + end + local custom_h = util.table_keys(itte.handlers) local codes = cxt.code_prefix .. table.concat(custom_h, ", " .. cxt.code_prefix) @@ -506,37 +765,23 @@ function itte._h.help(cxt, msg) ", " .. cxt.code_prefix .. table.concat(custom_h, ", " .. cxt.code_prefix) end - local help_msg = string.gsub(itte.config.messages.help, "{{codes}}", codes) - itte.message(cxt, { msg.reply_to }, help_msg) + local help_str = string.gsub(itte.config.messages.help, "{{codes}}", codes) + itte.message(cxt, { msg.reply_to }, help_str) end -itte.docs._h.join = [[ (context_table, message_table) - Join specified channels. +itte.docs._h.channels = [[ (context_table, message_table) + List the known channels for the current server. ]] -function itte._h.join(cxt, msg) - if not itte.is_admin(cxt.admins, msg) then - itte.notify_no_perms(cxt, msg) +function itte._h.channels(cxt, msg) + if not itte.is_allowed(cxt, msg) then do return end end - if msg.code_params ~= {} then - itte.traverse_channels(cxt, "join", msg.code_params) - itte.message(cxt, { msg.reply_to }, itte.config.messages.join) - end -end - -itte.docs._h.part = [[ (context_table, message_table) - Leave specified channels. - ]] -function itte._h.part(cxt, msg) - if not itte.is_admin(cxt.admins, msg) then - itte.notify_no_perms(cxt, msg) - do return end - end - if msg.code_params ~= {} then - itte.traverse_channels(cxt, "part", msg.code_params) - itte.message(cxt, { msg.reply_to }, itte.config.messages.part) + if cxt.channels ~= nil then + local channels_str = string.gsub(itte.config.messages.list_channels, + "{{channels}}", table.concat(cxt.channels, ", ")) + itte.message(cxt, { msg.reply_to }, channels_str) end end @@ -545,35 +790,14 @@ itte.docs._h.ping = [[ (context_table, message_table) Send a "pong" message. ]] function itte._h.ping(cxt, msg) + if not itte.is_allowed(cxt, msg) then + do return end + end + itte.message(cxt, { msg.reply_to }, itte.config.messages.ping) end -itte.docs._h.quit = [[ (context_table, message_table) - Disconnect from the server. - ]] -function itte._h.quit(cxt, msg) - if not itte.is_admin(itte.admins, msg, "global") then - itte.notify_no_perms(cxt, msg) - do return end - end - itte.send_command(cxt.con, cxt.cmds.quit.resp .. itte.config.messages.quit) -end - - -itte.docs._h.reload = [[ (context_table, message_table) - Reload the server config. - ]] -function itte._h.reload(cxt, msg) - if not itte.is_admin(itte.admins, msg, "global") then - itte.notify_no_perms(cxt, msg) - do return end - end - itte.get_config(true) - itte.message(cxt, { msg.reply_to }, itte.config.messages.reload) -end - - -- --------------------------------------------------------------------------- -- Service code mapping and runtime -- --------------------------------------------------------------------------- @@ -583,18 +807,26 @@ itte.docs.listen = [[ (name_str, data_str) string matches preset patterns. ]] function itte.listen(name, str) - util.debug("listen", str, itte.config.debug) - local cxt = itte.contexts[name] -- Respond to server ping if util.is_substr(str, cxt.cmds.ping.check) then + util.debug(itte.config.debugs.listen[1], str, itte.config.debug) itte.send_command(cxt.con, string.gsub(str, cxt.cmds.ping.check, cxt.cmds.ping.resp)) -- Respond to service code elseif util.is_substr(str, cxt.cmds.privmsg.check) then local msg = itte.parse_privmsg(cxt, str) + -- Check for the service in the admin handlers table and redact the debug + -- output for admin handlers. + if util.has_key(itte.config.admin_handlers, msg.code) then + util.debug(itte.config.debugs.listen[1], itte.config.debugs.redact[1], + itte.config.debug) + else + util.debug(itte.config.debugs.listen[1], str, itte.config.debug) + end + -- Check for the service code in the functions table before attempting to -- call the function. if util.has_key(itte._h, msg.code) then @@ -603,8 +835,8 @@ function itte.listen(name, str) elseif util.has_key(itte.handlers, msg.code) then itte.handlers[msg.code](cxt, msg) else - -- Only hint with unknown code error in private messages - -- as there may be prefix collision in channels. + -- Only hint with unknown code error in private messages as there may be + -- prefix collision in channels. if not util.is_substr(msg.reply_to, "#") then itte.message(cxt, { msg.reply_to }, itte.config.errors.unknown_code) end @@ -635,57 +867,12 @@ itte.docs.run = [[ () function itte.run() itte.get_config() - -- Connect to servers and get context with socket ref for each server - itte.contexts = {} - for instance, prop in pairs(itte.servers) do - itte.contexts[instance] = itte.connect_server(instance, prop) - - -- For PASS-based authentication, send PASS before greeting the server - if itte.contexts[instance].auth_type == "pass" then - itte.send_command(itte.contexts[instance].con, - itte.contexts[instance].cmds.pass.resp) - itte.contexts[instance].state.authed = true - end - - -- Greet the server in all auth scenarios except SASL, which greets the - -- server during CAP negotiation. - if (itte.contexts[instance].auth_type ~= "sasl") then - itte.send_command(itte.contexts[instance].con, - itte.contexts[instance].cmds.user.resp) - itte.send_command(itte.contexts[instance].con, - itte.contexts[instance].cmds.nick.resp) - end - - local joined_chans = false - while (itte.contexts[instance].state.connected) and (not joined_chans) do - local data, status = itte.contexts[instance].con:receive() - - if itte.contexts[instance].auth_type == "sasl" then - itte.negotiate_cap(itte.contexts[instance], data) - - elseif itte.contexts[instance].auth_type == "nickserv" then - itte.auth_nickserv(itte.contexts[instance], data) - end - - -- Minimum requirements for joining channels: client has authenticated if - -- an auth type is set, or has received a NOTICE [nick] message during an - -- ident lookup. - if itte.contexts[instance].state.authed then - itte.traverse_channels(itte.contexts[instance], "join") - joined_chans = true - elseif (data ~= nil) then - if util.is_substr(data, itte.contexts[instance].cmds.join.check) - then - itte.traverse_channels(itte.contexts[instance], "join") - joined_chans = true - end - end - end + for name, _ in pairs(itte.servers) do + itte.connect_server(name) end - -- Add listeners while itte.state.connected do - for instance, cxt in pairs(itte.contexts) do + for _, cxt in pairs(itte.contexts) do itte.add_listener(cxt) end end diff --git a/itteutil.lua b/itteutil.lua index d449dd4..63b7b9e 100644 --- a/itteutil.lua +++ b/itteutil.lua @@ -171,6 +171,32 @@ function itteutil.has_key(tbl, str) end +itteutil.docs.find_key = [[ (table, find_str) + Given a table and string, check whether the string is a key in the table. + Return the key if found, or nil otherwise. + ]] +function itteutil.find_key(tbl, str) + local keys = itteutil.table_keys(tbl) + -- Primitive check for array-like table with numerical keys + if (keys[1] == 1) and (keys[#keys] == #keys) then + for k = 1, #keys do + if str == tbl[k] then + do return k end + end + end + -- Associative table + else + for k = 1, #keys do + if str == keys[k] then + do return k end + end + end + end + return nil +end + + + itteutil.docs.is_entry = [[ (table, key_str, val_str) Given an associative table, a key and value pair as strings, check whether the pair is in the table. Return true if it exists in the table, or false @@ -243,10 +269,26 @@ function itteutil.source_file(str) if func then return func() else - print("Error: " .. err) + itteutil.debug("source_file", "Error: " .. err, true) do return end end end + +itteutil.docs.read_file = [[ (filename_str) + Open a file and return the contents as a string. + ]] +function itteutil.read_file(str) + local file = io.open(str) + if io.type(file) == "file" then + local contents = file:read("*a") + file:close() + return contents + else + itteutil.debug("read_file", "Error: cannot read file.", true) + end +end + + return itteutil