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 customisationtrunk
parent
7963d7221c
commit
75d3f149f2
|
@ -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
493
itte.lua
|
@ -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
|
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
|
|
||||||
|
|
||||||
-- 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
|
||||||
|
|
44
itteutil.lua
44
itteutil.lua
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue