Bugfixes
- Add global admins table, set disconnect and reload built-in handlers to be accessible by global admins only - Only send an unknown code error in private messages to avoid responding to other bots' codes when there is a prefix collision - Fix server contexts not being updated after a config reload - Fix service codes being improperly parsed in privmsgs when different code prefixes are used - Fix joining channels on some networks when connecting without authentication - Fix disconnecting from one server also causing disconnection from other serverstrunk
parent
9005c0fba5
commit
7963d7221c
|
@ -2,6 +2,16 @@
|
|||
-- Server configuration
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- `itte_admins`
|
||||
-- Users who can access administrative functions in the client, not limited to
|
||||
-- the server instance. The username is for the client only and independent of
|
||||
-- IRC network usernames.
|
||||
--
|
||||
-- This variable is optional, but certain handlers like server disconnection
|
||||
-- will not work if it is unset.
|
||||
itte_admins = { globaladmin = "password", }
|
||||
|
||||
|
||||
-- `itte_servers`
|
||||
-- Below is a sample server configuration.
|
||||
-- If the server does not support CAP negotiation (used for SASL
|
||||
|
@ -19,6 +29,6 @@ itte_servers = {
|
|||
auth_user = "botuser",
|
||||
auth_pass = "password",
|
||||
code_prefix = "!",
|
||||
admins = { adminuser = "password" },
|
||||
admins = { adminuser = "password", },
|
||||
},
|
||||
}
|
||||
|
|
196
itte.lua
196
itte.lua
|
@ -47,6 +47,9 @@ itte.servers = {}
|
|||
-- Store custom handlers
|
||||
itte.handlers = {}
|
||||
|
||||
-- Store global admins
|
||||
itte.admins = {}
|
||||
|
||||
-- Internal only: store core handlers
|
||||
itte._h = {}
|
||||
|
||||
|
@ -57,8 +60,8 @@ itte.docs._h = {}
|
|||
-- Internal only: track application state
|
||||
itte.state = {
|
||||
connected = false,
|
||||
connections = {},
|
||||
}
|
||||
itte.contexts = {}
|
||||
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
@ -79,7 +82,7 @@ end
|
|||
itte.docs.get_config = [[ ()
|
||||
Load the config file.
|
||||
]]
|
||||
function itte.get_config()
|
||||
function itte.get_config(reload)
|
||||
-- If prefix is not set, try custom file names or defaults
|
||||
if itte.confs.prefix == nil then
|
||||
util.source_file(itte.confs.config)
|
||||
|
@ -103,6 +106,20 @@ function itte.get_config()
|
|||
end
|
||||
if itte_config ~= nil then itte.config = itte_config 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.
|
||||
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)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
@ -153,9 +170,9 @@ function itte.get_commands(svr)
|
|||
},
|
||||
-- Main commands
|
||||
join = {
|
||||
-- 001 is a welcome message
|
||||
-- Cannot join earlier on some servers if not registered
|
||||
check = "001 " .. svr.nick,
|
||||
-- "MODE" seems to be a more reliable check
|
||||
check = "MODE",
|
||||
resp = "JOIN ",
|
||||
},
|
||||
nick = {
|
||||
|
@ -177,7 +194,7 @@ function itte.get_commands(svr)
|
|||
resp = "PONG ",
|
||||
},
|
||||
privmsg = {
|
||||
check = "PRIVMSG(.*):!",
|
||||
check = "PRIVMSG(.*):" .. svr.code_prefix,
|
||||
resp = "PRIVMSG ",
|
||||
},
|
||||
quit = {
|
||||
|
@ -214,14 +231,16 @@ function itte.message(cxt, users, str)
|
|||
end
|
||||
|
||||
|
||||
itte.docs.connect_server = [[ (server_table)
|
||||
itte.docs.connect_server = [[ (name_str, server_table)
|
||||
Initialise a socket connection to a server and return a context table.
|
||||
]]
|
||||
function itte.connect_server(svr)
|
||||
function itte.connect_server(name, svr)
|
||||
-- Load server context
|
||||
local context = svr
|
||||
context.name = name
|
||||
context.cmds = itte.get_commands(svr)
|
||||
context.state = {
|
||||
connected = false,
|
||||
cap_greeted = false,
|
||||
cap_ls = false,
|
||||
cap_checked = false,
|
||||
|
@ -239,16 +258,43 @@ function itte.connect_server(svr)
|
|||
verify = "none",
|
||||
options = "all",
|
||||
}
|
||||
|
||||
util.debug("conn", "Connecting to " .. 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()
|
||||
-- Set a short timeout to be non-blocking
|
||||
context.con:settimeout(0.1)
|
||||
|
||||
-- Set context and global states
|
||||
context.state.connected = true
|
||||
itte.state.connected = true
|
||||
|
||||
return context
|
||||
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.",
|
||||
itte.config.debug)
|
||||
end
|
||||
itte.contexts[name].con:close()
|
||||
itte.contexts[name] = nil
|
||||
|
||||
-- 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)
|
||||
itte.state.connected = false
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
itte.docs.negotiate_cap = [[ (context_table, data_str)
|
||||
Negotiate client capabilities and authenticate with SASL.
|
||||
]]
|
||||
|
@ -291,7 +337,7 @@ function itte.negotiate_cap(cxt, str)
|
|||
cxt.state.authed = true
|
||||
cxt.state.cap_checked = true
|
||||
if (cxt.state.cap_checked) and (not cxt.state.authed) and
|
||||
(itte.config.notify_errors) then
|
||||
(itte.config.notify_errors) and (cxt.admins ~= nil) then
|
||||
itte.send_command(cxt.con, cxt.cmds.cap_end.resp)
|
||||
itte.message(cxt, util.table_keys(cxt.admins),
|
||||
itte.config.errors.sasl_auth_failed)
|
||||
|
@ -315,7 +361,7 @@ function itte.auth_nickserv(cxt, str)
|
|||
elseif util.is_substr(str, cxt.cmds.ns_identify_pass.check) then
|
||||
cxt.state.authed = true
|
||||
if (cxt.state.ns_checked) and (not cxt.state.authed) and
|
||||
(itte.config.notify_errors) then
|
||||
(itte.config.notify_errors) and (cxt.admins ~= nil) then
|
||||
itte.message(cxt, util.table_keys(cxt.admins).
|
||||
itte.config.errors.ns_auth_failed)
|
||||
end
|
||||
|
@ -324,18 +370,29 @@ function itte.auth_nickserv(cxt, str)
|
|||
end
|
||||
|
||||
|
||||
itte.docs.is_admin = [[ (context_table, message_table)
|
||||
Check whether a user is an admin. Return true if the user and password are
|
||||
in the admin table, or false otherwise.
|
||||
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
|
||||
if the user and password are in the given admin table, or false otherwise.
|
||||
]]
|
||||
function itte.is_admin(cxt, msg)
|
||||
-- No password provided
|
||||
if table.concat(msg.code_params) == "" then
|
||||
function itte.is_admin(admins, msg, mode)
|
||||
local in_admins = false
|
||||
-- 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
|
||||
local in_admins = util.is_entry(cxt.admins, msg.sender,
|
||||
msg.code_params[#msg.code_params])
|
||||
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],
|
||||
msg.code_params[#msg.code_params])
|
||||
else
|
||||
-- User is the IRC user, password is suppled in the message
|
||||
in_admins = util.is_entry(admins, msg.sender,
|
||||
msg.code_params[#msg.code_params])
|
||||
end
|
||||
|
||||
if not in_admins then
|
||||
do return false end
|
||||
else
|
||||
|
@ -352,7 +409,7 @@ itte.docs.notify_no_perms = [[ (context_table, message_table)
|
|||
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 then
|
||||
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)
|
||||
|
@ -386,15 +443,22 @@ itte.docs.parse_privmsg = [[ (context_table, data_str)
|
|||
return an associative table of values.
|
||||
]]
|
||||
function itte.parse_privmsg(cxt, str)
|
||||
local code_full = string.sub(str, string.find(str, ":" .. cxt.code_prefix) +
|
||||
1 + string.len(cxt.code_prefix))
|
||||
local code_full = ""
|
||||
-- Separator marks the start of the message body
|
||||
body_sep, _ = string.find(str, ":", 2)
|
||||
-- Service code found with specified prefix
|
||||
if util.is_substr(str, ":" .. cxt.code_prefix) then
|
||||
code_full = string.sub(str, string.find(str, ":" ..
|
||||
cxt.code_prefix) + 1 + string.len(cxt.code_prefix))
|
||||
end
|
||||
local msg = {
|
||||
sender = string.sub(str, string.find(str, "!") + 2,
|
||||
string.find(str, "@") - 1),
|
||||
recipient = string.sub(str, string.find(str, "PRIVMSG") + 8,
|
||||
string.find(str, ":!") - 2),
|
||||
string.find(str, ":" .. cxt.code_prefix) - 2),
|
||||
reply_to = string.sub(str, string.find(str, "!") + 2,
|
||||
string.find(str, "@") - 1),
|
||||
body = string.sub(str, body_sep + 1),
|
||||
}
|
||||
if util.is_substr(code_full, " ") then
|
||||
msg.code = string.sub(code_full, 0, string.find(code_full, " ") - 1)
|
||||
|
@ -416,7 +480,8 @@ function itte.parse_privmsg(cxt, str)
|
|||
", recipient: " .. msg.recipient ..
|
||||
", reply_to: " .. msg.reply_to ..
|
||||
", code: " .. msg.code ..
|
||||
", code_params: " .. table.concat(msg.code_params) ..
|
||||
", code_params: " .. table.concat(msg.code_params, " ") ..
|
||||
", body: " .. msg.body ..
|
||||
" }", itte.config.debug)
|
||||
return msg
|
||||
end
|
||||
|
@ -435,7 +500,7 @@ function itte._h.help(cxt, msg)
|
|||
local codes = cxt.code_prefix .. table.concat(custom_h, ", " ..
|
||||
cxt.code_prefix)
|
||||
-- Core service codes are shown only to admins
|
||||
if itte.is_admin(cxt, msg) then
|
||||
if itte.is_admin(cxt.admins, msg) then
|
||||
local core_h = util.table_keys(itte._h)
|
||||
codes = cxt.code_prefix .. table.concat(core_h, ", " .. cxt.code_prefix) ..
|
||||
", " .. cxt.code_prefix .. table.concat(custom_h, ", " ..
|
||||
|
@ -450,7 +515,7 @@ itte.docs._h.join = [[ (context_table, message_table)
|
|||
Join specified channels.
|
||||
]]
|
||||
function itte._h.join(cxt, msg)
|
||||
if not itte.is_admin(cxt, msg) then
|
||||
if not itte.is_admin(cxt.admins, msg) then
|
||||
itte.notify_no_perms(cxt, msg)
|
||||
do return end
|
||||
end
|
||||
|
@ -465,7 +530,7 @@ itte.docs._h.part = [[ (context_table, message_table)
|
|||
Leave specified channels.
|
||||
]]
|
||||
function itte._h.part(cxt, msg)
|
||||
if not itte.is_admin(cxt, msg) then
|
||||
if not itte.is_admin(cxt.admins, msg) then
|
||||
itte.notify_no_perms(cxt, msg)
|
||||
do return end
|
||||
end
|
||||
|
@ -488,7 +553,7 @@ itte.docs._h.quit = [[ (context_table, message_table)
|
|||
Disconnect from the server.
|
||||
]]
|
||||
function itte._h.quit(cxt, msg)
|
||||
if not itte.is_admin(cxt, msg) then
|
||||
if not itte.is_admin(itte.admins, msg, "global") then
|
||||
itte.notify_no_perms(cxt, msg)
|
||||
do return end
|
||||
end
|
||||
|
@ -500,11 +565,11 @@ itte.docs._h.reload = [[ (context_table, message_table)
|
|||
Reload the server config.
|
||||
]]
|
||||
function itte._h.reload(cxt, msg)
|
||||
if not itte.is_admin(cxt, msg) then
|
||||
if not itte.is_admin(itte.admins, msg, "global") then
|
||||
itte.notify_no_perms(cxt, msg)
|
||||
do return end
|
||||
end
|
||||
itte.get_config()
|
||||
itte.get_config(true)
|
||||
itte.message(cxt, { msg.reply_to }, itte.config.messages.reload)
|
||||
end
|
||||
|
||||
|
@ -513,13 +578,15 @@ end
|
|||
-- Service code mapping and runtime
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
itte.docs.listen = [[ (context_table, data_str)
|
||||
itte.docs.listen = [[ (name_str, data_str)
|
||||
Parse the socket data string and trigger a response to the server if the
|
||||
string matches preset patterns.
|
||||
]]
|
||||
function itte.listen(cxt, str)
|
||||
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
|
||||
itte.send_command(cxt.con, string.gsub(str, cxt.cmds.ping.check,
|
||||
|
@ -536,7 +603,11 @@ function itte.listen(cxt, str)
|
|||
elseif util.has_key(itte.handlers, msg.code) then
|
||||
itte.handlers[msg.code](cxt, msg)
|
||||
else
|
||||
itte.message(cxt, { msg.reply_to }, itte.config.errors.unknown_code)
|
||||
-- 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
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -548,22 +619,12 @@ itte.docs.add_listener = [[ (context_table)
|
|||
set `state.connected` to false to activate a client exit.
|
||||
]]
|
||||
function itte.add_listener(cxt)
|
||||
-- Set a short timeout to be non-blocking
|
||||
cxt.con:settimeout(0.5)
|
||||
local data, status = cxt.con:receive()
|
||||
if data ~= nil then
|
||||
itte.listen(cxt, data)
|
||||
itte.listen(cxt.name, data)
|
||||
|
||||
elseif status == "closed" then
|
||||
util.debug("conn", "Connection " .. status, itte.config.debug)
|
||||
cxt.con:close()
|
||||
itte.state.connections[cxt.name] = nil
|
||||
-- Check if it is the last connection and trigger client exit
|
||||
-- if no connections remain.
|
||||
if #itte.state.connections == 0 then
|
||||
util.debug("conn", "Exiting ...", itte.config.debug)
|
||||
itte.state.connected = false
|
||||
end
|
||||
itte.disconnect_server(cxt.name)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -575,50 +636,47 @@ function itte.run()
|
|||
itte.get_config()
|
||||
|
||||
-- Connect to servers and get context with socket ref for each server
|
||||
local contexts = {}
|
||||
itte.contexts = {}
|
||||
for instance, prop in pairs(itte.servers) do
|
||||
contexts[instance] = itte.connect_server(prop)
|
||||
contexts[instance].name = instance
|
||||
itte.state.connected = true
|
||||
itte.state.connections[instance] = true
|
||||
contexts[instance].con:settimeout(1)
|
||||
itte.contexts[instance] = itte.connect_server(instance, prop)
|
||||
|
||||
-- For PASS-based authentication, send PASS before greeting the server
|
||||
if contexts[instance].auth_type == "pass" then
|
||||
itte.send_command(contexts[instance].con,
|
||||
contexts[instance].cmds.pass.resp)
|
||||
contexts[instance].state.authed = true
|
||||
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 (contexts[instance].auth_type ~= "sasl") then
|
||||
itte.send_command(contexts[instance].con,
|
||||
contexts[instance].cmds.user.resp)
|
||||
itte.send_command(contexts[instance].con,
|
||||
contexts[instance].cmds.nick.resp)
|
||||
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.state.connections[instance]) and (not joined_chans) do
|
||||
local data, status = contexts[instance].con:receive()
|
||||
while (itte.contexts[instance].state.connected) and (not joined_chans) do
|
||||
local data, status = itte.contexts[instance].con:receive()
|
||||
|
||||
if contexts[instance].auth_type == "sasl" then
|
||||
itte.negotiate_cap(contexts[instance], data)
|
||||
if itte.contexts[instance].auth_type == "sasl" then
|
||||
itte.negotiate_cap(itte.contexts[instance], data)
|
||||
|
||||
elseif contexts[instance].auth_type == "nickserv" then
|
||||
itte.auth_nickserv(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 contexts[instance].state.authed then
|
||||
itte.traverse_channels(contexts[instance], "join")
|
||||
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, contexts[instance].cmds.join.check) then
|
||||
itte.traverse_channels(contexts[instance], "join")
|
||||
if util.is_substr(data, itte.contexts[instance].cmds.join.check)
|
||||
then
|
||||
itte.traverse_channels(itte.contexts[instance], "join")
|
||||
joined_chans = true
|
||||
end
|
||||
end
|
||||
|
@ -627,7 +685,7 @@ function itte.run()
|
|||
|
||||
-- Add listeners
|
||||
while itte.state.connected do
|
||||
for instance, cxt in pairs(contexts) do
|
||||
for instance, cxt in pairs(itte.contexts) do
|
||||
itte.add_listener(cxt)
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue