Compare commits

...

3 Commits

3 changed files with 169 additions and 53 deletions

View File

@ -77,5 +77,12 @@ function h.hello(cxt, msg)
end end
-- This is a task handler for a task named "hello" in the sample config, and is
-- an alias to the hello() function above.
function h.th_hello(cxt, msg)
h.hello(cxt, msg)
end
-- Hook up the handlers. -- Hook up the handlers.
itte_handlers = h itte_handlers = h

View File

@ -34,7 +34,7 @@ itte_servers = {
admins = { demouser = "password", }, admins = { demouser = "password", },
-- Scheduled tasks. -- Scheduled tasks.
tasks = { tasks = {
task1 = { hello = {
-- Interval at which tasks are performed. Options: -- Interval at which tasks are performed. Options:
-- Every 5, 10, 15, 20 or 30 minutes — e.g. "5m" for 5 minutes -- Every 5, 10, 15, 20 or 30 minutes — e.g. "5m" for 5 minutes
-- hourly, daily, weekly, monthly -- hourly, daily, weekly, monthly
@ -44,7 +44,7 @@ itte_servers = {
-- at 00:00. -- at 00:00.
time = "12:00", time = "12:00",
-- The name of the handler to run for the task. -- The name of the handler to run for the task.
handler = "hello", handler = "th_hello",
-- The channels or users where the task handler will send messages. -- The channels or users where the task handler will send messages.
recipients = { "#channel1" }, recipients = { "#channel1" },
}, },

211
itte.lua
View File

@ -25,6 +25,7 @@ itte.config = {
debug = false, debug = false,
admin_handlers = { "connect", "quit", "reload", "servers", "join", "part" }, admin_handlers = { "connect", "quit", "reload", "servers", "join", "part" },
task_handler_prefix = "th_", task_handler_prefix = "th_",
target_handler = "target",
messages = { messages = {
connect_success = "Connected to {{server}}.", connect_success = "Connected to {{server}}.",
help = "Service codes available: {{codes}}", help = "Service codes available: {{codes}}",
@ -51,9 +52,10 @@ itte.config = {
dc_svr = { "conn", "Connection to {{server}} closed." }, dc_svr = { "conn", "Connection to {{server}} closed." },
exit = { "conn", "Exiting ..." }, exit = { "conn", "Exiting ..." },
listen = { "listen" }, listen = { "listen" },
names = { "names", "channel", "users" },
nickserv = { "auth_nickserv" }, nickserv = { "auth_nickserv" },
privmsg = { "sender", "recipient", "reply_to", "code", "code_params", privmsg = { "privmsg", "sender", "recipient", "reply_to", "code",
"body" }, "code_params", "body" },
redact = { "********" }, redact = { "********" },
send = { "send" }, send = { "send" },
svrs_not_found = { "config", "Error: servers not found." }, svrs_not_found = { "config", "Error: servers not found." },
@ -217,6 +219,11 @@ function itte.get_commands(svr)
check = "MODE", check = "MODE",
resp = "JOIN ", resp = "JOIN ",
}, },
names = {
-- Check and response are reversed
check = "353",
resp = "NAMES",
},
nick = { nick = {
check = "NOTICE", check = "NOTICE",
resp = "NICK " .. svr.nick, resp = "NICK " .. svr.nick,
@ -243,6 +250,18 @@ function itte.get_commands(svr)
check = nil, check = nil,
resp = "QUIT :", resp = "QUIT :",
}, },
target_private = {
-- Like privmsg but targets bot nick in a direct message
-- Escape magic character "-"
check = "PRIVMSG(.*)" .. string.gsub(svr.nick, "-", "%%-") .. ":",
resp = "PRIVMSG ",
},
target_public = {
-- Like privmsg but targets bot nick in a channel
-- Escape magic character "-"
check = "PRIVMSG(.*)#(.*):" .. string.gsub(svr.nick, "-", "%%-"),
resp = "PRIVMSG ",
},
user = { user = {
check = "NOTICE", check = "NOTICE",
resp = "USER " .. svr.auth_user .. " 0 * " .. svr.auth_user, resp = "USER " .. svr.auth_user .. " 0 * " .. svr.auth_user,
@ -551,6 +570,7 @@ itte.docs.traverse_channels = [[ (context_table, mode_str, channels_table)
]] ]]
function itte.traverse_channels(cxt, mode, chans) function itte.traverse_channels(cxt, mode, chans)
local channels = cxt.channels local channels = cxt.channels
cxt.chan_meta = {}
if chans ~= nil then channels = chans end if chans ~= nil then channels = chans end
if mode == "join" then if mode == "join" then
@ -560,6 +580,7 @@ function itte.traverse_channels(cxt, mode, chans)
for c = 1, #channels do for c = 1, #channels do
if not util.has_key(cxt.channels, channels[c]) then if not util.has_key(cxt.channels, channels[c]) then
table.insert(cxt.channels, channels[c]) table.insert(cxt.channels, channels[c])
cxt.chan_meta[channels[c]:sub(2)] = {}
end end
end end
@ -570,68 +591,136 @@ function itte.traverse_channels(cxt, mode, chans)
for c = 1, #channels do for c = 1, #channels do
if util.has_key(cxt.channels, channels[c]) then if util.has_key(cxt.channels, channels[c]) then
table.remove(cxt.channels, util.find_key(cxt.channels, channels[c])) table.remove(cxt.channels, util.find_key(cxt.channels, channels[c]))
cxt.chan_meta[channels[c]:sub(2)] = nil
end end
end end
end end
end end
itte.docs.parse_privmsg = [[ (context_table, data_str) itte.docs.parse_privmsg = [[ (context_table, data_str [, mode_str])
Given the context table and a PRIVMSG data string, parse the string and Given the context table, a PRIVMSG data string and mode, parse the string and
return an associative table of values. return an associative table of values. If "code" mode is specified, it will
check the body for a service code. If "target" mode is specified, it will
extract the message body excluding the bot nick, but will not look for a
service code. The default mode is "code" if no mode is specified.
]] ]]
function itte.parse_privmsg(cxt, str) function itte.parse_privmsg(cxt, str, mode)
local code_full = "" local msg = {}
-- Separator marks the start of the message body -- Separator marks the start of the message body
body_sep, _ = string.find(str, ":", 2) 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, ":" .. 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)
msg.code_params = util.split_str(string.sub(code_full,
string.find(code_full, " ") + 1))
-- Message in the format `[nick]: [body]`
if mode == "target" then
msg = {
sender = string.sub(str, string.find(str, "!") + 2,
string.find(str, "@") - 1),
-- Escape magic character "-" in the nick
recipient = string.sub(str, string.find(str, "PRIVMSG") + 8,
string.find(str, ":" .. string.gsub(cxt.nick, "-", "%%-")) - 2),
reply_to = string.sub(str, string.find(str, "!") + 2,
string.find(str, "@") - 1),
body = string.gsub(string.sub(str, body_sep + 2 +
string.len(cxt.nick)), "^%s", ""),
}
-- Reply to sender by default (private message), or reply in a channel if
-- original message recipient is a channel.
if util.is_substr(msg.recipient, "#") then
msg.reply_to = msg.recipient
end
util.debug(itte.config.debugs.privmsg[1], "{ " ..
itte.config.debugs.privmsg[2] .. ": " .. msg.sender ..
", " .. itte.config.debugs.privmsg[3] .. ": " .. msg.recipient ..
", " .. itte.config.debugs.privmsg[4] .. ": " .. msg.reply_to ..
", " .. itte.config.debugs.privmsg[7] .. ": " .. msg.body ..
" }", itte.config.debug)
-- Message in the format `[code] [params]`
else else
msg.code = code_full local code_full = ""
msg.code_params = {} -- Service code found with specified prefix
end if util.is_substr(str, ":" .. cxt.code_prefix) then
-- Reply to sender by default (private message), or reply in a channel if code_full = string.sub(str, string.find(str, ":" ..
-- original message recipient is a channel. cxt.code_prefix) + 1 + string.len(cxt.code_prefix))
if util.is_substr(msg.recipient, "#") then end
msg.reply_to = msg.recipient msg = {
end sender = string.sub(str, string.find(str, "!") + 2,
string.find(str, "@") - 1),
recipient = string.sub(str, string.find(str, "PRIVMSG") + 8,
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)
msg.code_params = util.split_str(string.sub(code_full,
string.find(code_full, " ") + 1))
-- Redact debug output for admin-only handlers to avoid logging passwords. else
local body = msg.body msg.code = code_full
local code_params = table.concat(msg.code_params, " ") msg.code_params = {}
if util.has_key(itte.config.admin_handlers, msg.code) then end
body = itte.config.debugs.redact[1] -- Reply to sender by default (private message), or reply in a channel if
code_params = itte.config.debugs.redact[1] -- original message recipient is a channel.
if util.is_substr(msg.recipient, "#") then
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(itte.config.debugs.privmsg[1], "{ " ..
itte.config.debugs.privmsg[2] .. ": " .. msg.sender ..
", " .. itte.config.debugs.privmsg[3] .. ": " .. msg.recipient ..
", " .. itte.config.debugs.privmsg[4] .. ": " .. msg.reply_to ..
", " .. itte.config.debugs.privmsg[5] .. ": " .. msg.code ..
", " .. itte.config.debugs.privmsg[6] .. ": " .. code_params ..
", " .. itte.config.debugs.privmsg[7] .. ": " .. body ..
" }", itte.config.debug)
end end
util.debug("privmsg", "{ " ..
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 return msg
end end
itte.docs.parse_names = [[ (context_table, data_str)
Given the context table and a NAMES data string, parse the string and
return the channel name and a table of users.
]]
function itte.parse_names(cxt, str)
-- Separator marks the start of the message body
body_sep, _ = string.find(str, ":", 2)
local channel = string.sub(str, string.find(str, "#"),
string.find(str, ":", 2) - 2)
local names = util.split_str(string.sub(str, body_sep + 1))
util.debug(itte.config.debugs.names[1],
itte.config.debugs.names[2] .. ": " .. channel ..
", " .. itte.config.debugs.names[3] .. ": { " .. table.concat(names, ", ")
.. " }", itte.config.debug)
return channel, names
end
itte.docs.get_users = [[ (context_table, channel_str)
Given the context table and a channel, return a table of users in the
channel.
]]
function itte.get_users(cxt, str)
local chan = str
if string.find(chan, "#") == 1 then chan = str:sub(2) end
if not util.has_key(cxt.chan_meta, chan) then
do return end
end
-- Query server for the latest list of names
itte.send_command(cxt.con, cxt.cmds.names.resp .. " #" .. chan)
return cxt.chan_meta[chan].users
end
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
-- Service code handlers -- Service code handlers
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
@ -775,9 +864,10 @@ function itte._h.help(cxt, msg)
end end
local custom_h = util.table_keys(itte.handlers) local custom_h = util.table_keys(itte.handlers)
-- Remove task handlers -- Remove target and task handlers
for h = 1, #custom_h do for h = 1, #custom_h do
if string.find(custom_h[h], itte.config.task_handler_prefix) == 1 then if (string.find(custom_h[h], itte.config.target_handler) == 1) or
(string.find(custom_h[h], itte.config.task_handler_prefix) == 1) then
custom_h[h] = nil custom_h[h] = nil
end end
end end
@ -840,6 +930,13 @@ function itte.listen(name, str)
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))
-- Update channel names list
elseif string.find(str, ":" .. cxt.host .. " " .. cxt.cmds.names.check) == 1
then
util.debug(itte.config.debugs.listen[1], str, itte.config.debug)
local channel, names = itte.parse_names(cxt, str)
cxt.chan_meta[channel:sub(2)] = { users = names }
-- 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)
@ -852,8 +949,9 @@ function itte.listen(name, str)
util.debug(itte.config.debugs.listen[1], str, itte.config.debug) util.debug(itte.config.debugs.listen[1], str, itte.config.debug)
end end
-- Ignore calls to task handlers -- Ignore calls to the target and task handlers
if (string.find(msg.code, itte.config.task_handler_prefix) == 1) then if (string.find(msg.code, itte.config.target_handler) == 1) or
(string.find(msg.code, itte.config.task_handler_prefix) == 1) then
do return end do return 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.
@ -870,6 +968,17 @@ function itte.listen(name, str)
end end
end end
-- Respond to direct address in a channel
elseif (util.is_substr(str, cxt.cmds.target_public.check)) or
(util.is_substr(str, cxt.cmds.target_private.check)) then
util.debug(itte.config.debugs.listen[1], str, itte.config.debug)
local msg = itte.parse_privmsg(cxt, str, "target")
-- Check for the target handler function before attempting to call it.
if util.has_key(itte.handlers, itte.config.target_handler) then
itte.handlers[itte.config.target_handler](cxt, msg)
end
-- Output to stdout anyway if debug is enabled -- Output to stdout anyway if debug is enabled
else else
util.debug(itte.config.debugs.listen[1], str, itte.config.debug) util.debug(itte.config.debugs.listen[1], str, itte.config.debug)