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
trunk
mio 2022-03-20 03:42:25 +00:00
parent 7963d7221c
commit 75d3f149f2
3 changed files with 383 additions and 155 deletions

View File

@ -381,7 +381,6 @@ end
-- ]] -- ]]
itte_config = { itte_config = {
debug = true, debug = true,
notify_errors = true,
messages = { messages = {
help = "一、二、三、らーめん缶! Hello, I am a ramen vending machine. " .. help = "一、二、三、らーめん缶! Hello, I am a ramen vending machine. " ..
"Please type a code for service: {{codes}} " .. "Please type a code for service: {{codes}} " ..

493
itte.lua
View File

@ -23,22 +23,40 @@ itte.confs = {
-- Default application settings -- Default application settings
itte.config = { itte.config = {
debug = false, debug = false,
notify_errors = false, admin_handlers = { "connect", "quit", "reload", "servers", "join", "part" },
messages = { messages = {
connect_success = "Connected to {{server}}.",
help = "Service codes available: {{codes}}", help = "Service codes available: {{codes}}",
join = "We're in.", 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.", part = "The bot is greater than the sum of its parts.",
ping = "Pong!", ping = "Pong!",
quit = "Goodbye.", quit = "Goodbye.",
quit_success = "Quit {{server}}.",
reload = "Splines recticulated.", reload = "Splines recticulated.",
}, },
errors = { errors = {
invalid_name = "Error: invalid server name `{{server}}`.",
no_perm = "I'm sorry, {{user}}. I'm afraid I can't do that.", 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.", ns_auth_failed = "Error: Nickserv identification failed.",
sasl_auth_failed = "Error: SASL authentication failed.", sasl_auth_failed = "Error: SASL authentication failed.",
unknown_code = "I don't understand what you just said.", 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 -- Store server info
@ -79,7 +97,7 @@ function itte.help(name)
end end
itte.docs.get_config = [[ () itte.docs.get_config = [[ ([reload_bool])
Load the config file. Load the config file.
]] ]]
function itte.get_config(reload) function itte.get_config(reload)
@ -101,23 +119,29 @@ function itte.get_config(reload)
if itte.servers ~= nil then if itte.servers ~= nil then
itte.servers = itte_servers itte.servers = itte_servers
else 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 do return end
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_handlers ~= nil then itte.handlers = itte_handlers end
if itte_admins ~= nil then itte.admins = itte_admins end if itte_admins ~= nil then itte.admins = itte_admins end
-- If reloading, reconstruct the context tables. -- 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 if reload then
for instance, prop in pairs(itte.servers) do for name, prop in pairs(itte.servers) do
cxt_old = itte.contexts[instance] cxt_old = itte.contexts[name]
itte.contexts[instance] = prop itte.contexts[name] = prop
itte.contexts[instance].name = instance itte.contexts[name].name = name
itte.contexts[instance].cmds = itte.get_commands(prop) itte.contexts[name].cmds = itte.get_commands(prop)
itte.contexts[instance].con = cxt_old.con itte.contexts[name].con = cxt_old.con
itte.contexts[instance].con:settimeout(0.5) itte.contexts[name].con:settimeout(0.5)
end end
end end
end end
@ -211,12 +235,18 @@ function itte.get_commands(svr)
end 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. 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") 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 end
@ -231,10 +261,10 @@ function itte.message(cxt, users, str)
end 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. 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 -- Load server context
local context = svr local context = svr
context.name = name context.name = name
@ -259,8 +289,9 @@ function itte.connect_server(name, svr)
options = "all", options = "all",
} }
util.debug("conn", "Connecting to " .. svr.host .. "/" .. svr.port .. util.debug(itte.config.debugs.conn_svr[1],
" ...", itte.config.debug) 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:connect(svr.host, svr.port)
context.con = ssl.wrap(context.con, con_params) context.con = ssl.wrap(context.con, con_params)
context.con:dohandshake() context.con:dohandshake()
@ -275,12 +306,63 @@ function itte.connect_server(name, svr)
end 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) itte.docs.disconnect_server = [[ (name_str)
Close a socket connection to a server. Close a socket connection to a server.
]] ]]
function itte.disconnect_server(name) function itte.disconnect_server(name)
if itte.contexts[name] ~= nil then 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) itte.config.debug)
end end
itte.contexts[name].con:close() 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 -- Check if it is the last connection and trigger client exit
-- if no connections remain -- if no connections remain
if #util.table_keys(itte.contexts) == 0 then 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 itte.state.connected = false
end end
end end
@ -307,7 +390,7 @@ function itte.negotiate_cap(cxt, str)
end end
if (str ~= nil) then 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 -- Wait for server to respond with capabilities list before greeting
if (not cxt.state.cap_greeted) then 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" -- Format of the string to encode: "user\0user\0password"
local sasl_pass, _ = mime.b64(cxt.auth_user .. "\0" .. cxt.auth_user .. local sasl_pass, _ = mime.b64(cxt.auth_user .. "\0" .. cxt.auth_user ..
"\0" .. cxt.auth_pass) "\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 -- Look for auth success signal and end cap negotiation
elseif (util.is_substr(str, cxt.cmds.cap_end.check)) then 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) function itte.auth_nickserv(cxt, str)
if str ~= nil then 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 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 cxt.state.ns_checked = true
elseif util.is_substr(str, cxt.cmds.ns_identify_pass.check) then elseif util.is_substr(str, cxt.cmds.ns_identify_pass.check) then
cxt.state.authed = true cxt.state.authed = true
@ -370,9 +453,22 @@ function itte.auth_nickserv(cxt, str)
end end
itte.docs.is_admin = [[ (admins_table, message_table, mode_str) itte.docs.is_pm = [[ (message_table)
Check whether a user is an admin. Modes: global, instance. If `mode` is Check whether a message is a private message. Return true if it is a private
unspecified, check for the user in the instance's admins table. Return true 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. if the user and password are in the given admin table, or false otherwise.
]] ]]
function itte.is_admin(admins, msg, mode) 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 no admin users set or no password provided
if (admins == nil) or (table.concat(msg.code_params) == "") then if (admins == nil) or (table.concat(msg.code_params) == "") then
do return false end do return false end
-- Incorrect password provided
elseif msg.code_params ~= nil then elseif msg.code_params ~= nil then
if mode == "global" then -- No password provided in global mode
-- For global admins, both user and password are supplied in the message if (mode == "global") and (#util.table_keys(msg.code_params) < 2) then
-- to keep the auth network-independent do return false end
in_admins = util.is_entry(admins, msg.code_params[1], 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]) msg.code_params[#msg.code_params])
else else
-- User is the IRC user, password is suppled in the message -- 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]) msg.code_params[#msg.code_params])
end end
-- Incorrect password provided
if not in_admins then if not in_admins then
do return false end do return false end
else else
@ -402,18 +502,25 @@ function itte.is_admin(admins, msg, mode)
end end
itte.docs.notify_no_perms = [[ (context_table, message_table) itte.docs.is_allowed = [[ (context_table, message_table [, mode_str])
Respond to an attempt to use service code by a non-admin user and send a Check if user is allowed to run a handler. If the handler is for admins only,
message to the admins if `notify_errors` is enabled in the config. 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) function itte.is_allowed(cxt, msg, mode)
itte.message(cxt, { msg.reply_to }, string.gsub(itte.config.errors.no_perm, if not util.has_key(itte.config.admin_handlers, msg.code) then
"{{user}}", msg.sender)) return true
if (itte.config.notify_errors) and (cxt.admins ~= nil) then else
local notify_msg = string.gsub(itte.config.errors.notify_no_perm, local admins = cxt.admins
"{{user}}", msg.sender) if mode == "global" then admins = itte.admins end
notify_msg = string.gsub(notify_msg, "{{code}}", msg.code) if (itte.is_pm(msg)) and itte.is_admin(admins, msg, mode) then
itte.message(cxt, util.table_keys(cxt.admins), notify_msg) return true
else
itte.message(cxt, { msg.reply_to },
string.gsub(itte.config.errors.no_perm, "{{user}}", msg.sender))
return false
end
end end
end end
@ -430,10 +537,22 @@ function itte.traverse_channels(cxt, mode, chans)
if mode == "join" then if mode == "join" then
itte.send_command(cxt.con, cxt.cmds.join.resp .. table.concat(channels, 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 elseif mode == "part" then
itte.send_command(cxt.con, cxt.cmds.part.resp .. table.concat(channels, 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
end end
@ -475,13 +594,20 @@ function itte.parse_privmsg(cxt, str)
msg.reply_to = msg.recipient msg.reply_to = msg.recipient
end 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", "{ " .. util.debug("privmsg", "{ " ..
"sender: " .. msg.sender .. itte.config.debugs.privmsg[1] .. ": " .. msg.sender ..
", recipient: " .. msg.recipient .. ", " .. itte.config.debugs.privmsg[2] .. ": " .. msg.recipient ..
", reply_to: " .. msg.reply_to .. ", " .. itte.config.debugs.privmsg[3] .. ": " .. msg.reply_to ..
", code: " .. msg.code .. ", " .. itte.config.debugs.privmsg[4] .. ": " .. msg.code ..
", code_params: " .. table.concat(msg.code_params, " ") .. ", " .. itte.config.debugs.privmsg[5] .. ": " .. code_params ..
", body: " .. msg.body .. ", " .. itte.config.debugs.privmsg[6] .. ": " .. body ..
" }", itte.config.debug) " }", itte.config.debug)
return msg return msg
end end
@ -491,11 +617,144 @@ end
-- Service code handlers -- 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) itte.docs._h.help = [[ (context_table, message_table)
Send a help message listing available service codes. Send a help message listing available service codes.
]] ]]
-- Send a help message listing available service codes.
function itte._h.help(cxt, msg) 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 custom_h = util.table_keys(itte.handlers)
local codes = cxt.code_prefix .. table.concat(custom_h, ", " .. local codes = cxt.code_prefix .. table.concat(custom_h, ", " ..
cxt.code_prefix) cxt.code_prefix)
@ -506,37 +765,23 @@ function itte._h.help(cxt, msg)
", " .. cxt.code_prefix .. table.concat(custom_h, ", " .. ", " .. cxt.code_prefix .. table.concat(custom_h, ", " ..
cxt.code_prefix) cxt.code_prefix)
end end
local help_msg = string.gsub(itte.config.messages.help, "{{codes}}", codes) local help_str = string.gsub(itte.config.messages.help, "{{codes}}", codes)
itte.message(cxt, { msg.reply_to }, help_msg) itte.message(cxt, { msg.reply_to }, help_str)
end end
itte.docs._h.join = [[ (context_table, message_table) itte.docs._h.channels = [[ (context_table, message_table)
Join specified channels. List the known channels for the current server.
]] ]]
function itte._h.join(cxt, msg) function itte._h.channels(cxt, msg)
if not itte.is_admin(cxt.admins, msg) then if not itte.is_allowed(cxt, msg) then
itte.notify_no_perms(cxt, msg)
do return end do return end
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
if cxt.channels ~= nil then
itte.docs._h.part = [[ (context_table, message_table) local channels_str = string.gsub(itte.config.messages.list_channels,
Leave specified channels. "{{channels}}", table.concat(cxt.channels, ", "))
]] itte.message(cxt, { msg.reply_to }, channels_str)
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)
end end
end end
@ -545,35 +790,14 @@ itte.docs._h.ping = [[ (context_table, message_table)
Send a "pong" message. Send a "pong" message.
]] ]]
function itte._h.ping(cxt, msg) 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) itte.message(cxt, { msg.reply_to }, itte.config.messages.ping)
end 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 -- Service code mapping and runtime
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
@ -583,18 +807,26 @@ itte.docs.listen = [[ (name_str, data_str)
string matches preset patterns. string matches preset patterns.
]] ]]
function itte.listen(name, str) function itte.listen(name, str)
util.debug("listen", str, itte.config.debug)
local cxt = itte.contexts[name] local cxt = itte.contexts[name]
-- Respond to server ping -- Respond to server ping
if util.is_substr(str, cxt.cmds.ping.check) then 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, itte.send_command(cxt.con, string.gsub(str, cxt.cmds.ping.check,
cxt.cmds.ping.resp)) cxt.cmds.ping.resp))
-- Respond to service code -- Respond to service code
elseif util.is_substr(str, cxt.cmds.privmsg.check) then elseif util.is_substr(str, cxt.cmds.privmsg.check) then
local msg = itte.parse_privmsg(cxt, str) 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 -- Check for the service code in the functions table before attempting to
-- call the function. -- call the function.
if util.has_key(itte._h, msg.code) then 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 elseif util.has_key(itte.handlers, msg.code) then
itte.handlers[msg.code](cxt, msg) itte.handlers[msg.code](cxt, msg)
else else
-- Only hint with unknown code error in private messages -- Only hint with unknown code error in private messages as there may be
-- as there may be prefix collision in channels. -- prefix collision in channels.
if not util.is_substr(msg.reply_to, "#") then if not util.is_substr(msg.reply_to, "#") then
itte.message(cxt, { msg.reply_to }, itte.config.errors.unknown_code) itte.message(cxt, { msg.reply_to }, itte.config.errors.unknown_code)
end end
@ -635,57 +867,12 @@ itte.docs.run = [[ ()
function itte.run() function itte.run()
itte.get_config() itte.get_config()
-- Connect to servers and get context with socket ref for each server for name, _ in pairs(itte.servers) do
itte.contexts = {} itte.connect_server(name)
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
end end
-- Add listeners
while itte.state.connected do while itte.state.connected do
for instance, cxt in pairs(itte.contexts) do for _, cxt in pairs(itte.contexts) do
itte.add_listener(cxt) itte.add_listener(cxt)
end end
end end

View File

@ -171,6 +171,32 @@ function itteutil.has_key(tbl, str)
end 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) itteutil.docs.is_entry = [[ (table, key_str, val_str)
Given an associative table, a key and value pair as strings, check whether 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 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 if func then
return func() return func()
else else
print("Error: " .. err) itteutil.debug("source_file", "Error: " .. err, true)
do return end do return end
end 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 return itteutil