Compare commits

...

1 Commits

Author SHA1 Message Date
mio e9927fb1d7 Add scheduled tasks 2022-04-01 04:14:15 -04:00
4 changed files with 149 additions and 16 deletions

View File

@ -47,7 +47,8 @@
- `itte_handlers`: most bots listen for some pre-defined code words (often - `itte_handlers`: most bots listen for some pre-defined code words (often
known as "commands") and respond by calling a handler, or function to known as "commands") and respond by calling a handler, or function to
handle the request accordingly. handle the request accordingly. Handlers for scheduled tasks can also be
added.
The module will refer to the keywords as codes to distinguish them The module will refer to the keywords as codes to distinguish them
from IRC commands sent to the server like `JOIN` or `PART`. Name the from IRC commands sent to the server like `JOIN` or `PART`. Name the
@ -119,7 +120,8 @@ The `examples/` directory has a few demo bots to show how to use the module.
- *hellobot*: a "hello world" example that greets users in several different - *hellobot*: a "hello world" example that greets users in several different
languages when they type `!hello`. languages when they type `!hello`.
- *ramenkan*: a bot that serves ramen. It has some custom config settings. - *ramenkan*: a bot that serves ramen. It has some custom config settings and
scheduled tasks.
- *hachi*: a bot inspired by tracery text expansion. Loads files ("foils") - *hachi*: a bot inspired by tracery text expansion. Loads files ("foils")
containing selection tables and creates handlers to return responses, one containing selection tables and creates handlers to return responses, one

View File

@ -423,14 +423,22 @@ local h = {}
-- Serve ramen. -- Serve ramen.
function h.ramen(cxt, msg) function h.ramen(cxt, msg)
local recipients = { msg.reply_to }
local intro = ""
-- Scheduled task recipients
if msg.recipients ~= nil then
recipients = msg.recipients
intro = "Now serving! "
end
-- 1% chance of special set -- 1% chance of special set
local roll = math.random(1, 100) local roll = math.random(1, 100)
if roll == 77 then if roll == 77 then
irc.message(cxt, { msg.reply_to }, irc.message(cxt, recipients, intro ..
string.gsub(itte_config.messages.special, "{{ramen}}", string.gsub(itte_config.messages.special, "{{ramen}}",
ramen.make_ramen("veg_special"))) ramen.make_ramen("veg_special")))
else else
irc.message(cxt, { msg.reply_to }, irc.message(cxt, recipients, intro ..
string.gsub(itte_config.messages.serve, "{{ramen}}", string.gsub(itte_config.messages.serve, "{{ramen}}",
ramen.make_ramen("veg"))) ramen.make_ramen("veg")))
end end
@ -474,4 +482,10 @@ function h.water(cxt, msg)
end end
-- Serve ramen at the interval specified in the config.
function h.th_ramen(cxt, task)
h.ramen(cxt, task)
end
itte_handlers = h itte_handlers = h

View File

@ -9,6 +9,14 @@ itte_servers = {
auth_user = "USER", auth_user = "USER",
code_prefix = "!", code_prefix = "!",
admins = { USER = "PASSWORD" }, admins = { USER = "PASSWORD" },
tasks = {
ramen = {
interval = "daily",
time = "12:00",
handler = "th_ramen",
recipients = { "#ramenkan" },
},
},
}, },
casa = { casa = {
host = "m455.casa", host = "m455.casa",
@ -21,6 +29,14 @@ itte_servers = {
auth_pass = "PASSWORD", auth_pass = "PASSWORD",
code_prefix = "!", code_prefix = "!",
admins = { USER = "PASSWORD" }, admins = { USER = "PASSWORD" },
tasks = {
ramen = {
interval = "daily",
time = "17:00",
handler = "th_ramen",
recipients = { "#kitchen" },
},
},
}, },
tildechat = { tildechat = {
host = "irc.tilde.chat", host = "irc.tilde.chat",

117
itte.lua
View File

@ -24,6 +24,7 @@ itte.confs = {
itte.config = { 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_",
messages = { messages = {
connect_success = "Connected to {{server}}.", connect_success = "Connected to {{server}}.",
help = "Service codes available: {{codes}}", help = "Service codes available: {{codes}}",
@ -56,6 +57,8 @@ itte.config = {
redact = { "********" }, redact = { "********" },
send = { "send" }, send = { "send" },
svrs_not_found = { "config", "Error: servers not found." }, svrs_not_found = { "config", "Error: servers not found." },
task_added = { "task", "Task `{{name}}` added." },
task_activated = { "task", "Task `{{name}}` activated at {{ts}}." },
}, },
} }
@ -78,6 +81,7 @@ itte.docs._h = {}
-- Internal only: track application state -- Internal only: track application state
itte.state = { itte.state = {
connected = false, connected = false,
timeout = 0.1,
} }
itte.contexts = {} itte.contexts = {}
@ -143,16 +147,19 @@ function itte.get_config(reload)
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.
-- This only works if the name name has not changed between reloads.
if reload then if reload then
-- Reconstruct the context tables
-- This only works if the server name has not changed between reloads.
for name, prop in pairs(itte.servers) do for name, prop in pairs(itte.servers) do
cxt_old = itte.contexts[name] cxt_old = itte.contexts[name]
itte.contexts[name] = prop itte.contexts[name] = prop
itte.contexts[name].name = name itte.contexts[name].name = name
itte.contexts[name].cmds = itte.get_commands(prop) itte.contexts[name].cmds = itte.get_commands(prop)
itte.contexts[name].con = cxt_old.con itte.contexts[name].con = cxt_old.con
itte.contexts[name].con:settimeout(0.5) itte.contexts[name].con:settimeout(itte.state.timeout)
-- Reconstruct the task coroutines
itte.add_tasks(itte.contexts[name])
end end
end end
end end
@ -307,7 +314,7 @@ function itte.init_socket(name, svr)
context.con = ssl.wrap(context.con, con_params) context.con = ssl.wrap(context.con, con_params)
context.con:dohandshake() context.con:dohandshake()
-- Set a short timeout to be non-blocking -- Set a short timeout to be non-blocking
context.con:settimeout(0.1) context.con:settimeout(itte.state.timeout)
-- Set context and global states -- Set context and global states
context.state.connected = true context.state.connected = true
@ -385,6 +392,7 @@ function itte.disconnect_server(name)
util.debug(itte.config.debugs.exit[1], itte.config.debugs.exit[2], util.debug(itte.config.debugs.exit[1], itte.config.debugs.exit[2],
itte.config.debug) itte.config.debug)
itte.state.connected = false itte.state.connected = false
os.exit()
end end
end end
@ -767,6 +775,12 @@ 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
for h = 1, #custom_h do
if string.find(custom_h[h], itte.config.task_handler_prefix) == 1 then
custom_h[h] = nil
end
end
local codes = cxt.code_prefix .. table.concat(custom_h, ", " .. local codes = cxt.code_prefix .. table.concat(custom_h, ", " ..
cxt.code_prefix) cxt.code_prefix)
-- Core service codes are shown only to admins -- Core service codes are shown only to admins
@ -810,7 +824,7 @@ end
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
-- Service code mapping and runtime -- Service code and taskd task mapping
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
itte.docs.listen = [[ (name_str, data_str) itte.docs.listen = [[ (name_str, data_str)
@ -838,9 +852,12 @@ 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
if (string.find(msg.code, itte.config.task_handler_prefix) == 1) then
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.
if util.has_key(itte._h, msg.code) then elseif util.has_key(itte._h, msg.code) then
itte._h[msg.code](cxt, msg) itte._h[msg.code](cxt, msg)
-- Check the handlers table -- Check the handlers table
elseif util.has_key(itte.handlers, msg.code) then elseif util.has_key(itte.handlers, msg.code) then
@ -872,6 +889,83 @@ function itte.add_listener(cxt)
end end
itte.docs.add_tasks = [[ (context_table)
Create coroutines for tasks.
]]
function itte.add_tasks(cxt)
-- Exit function if there are no tasks for the context
if (cxt.tasks == nil) or (cxt.tasks == {}) then
do return end
end
-- Construct a task coroutine and call its handler function when resumed
local co = function(name, task)
if util.has_key(itte.handlers, task.handler) then
local ts = os.date("%Y-%m-%d %H:%M:%S", os.time())
local task_str = string.gsub(itte.config.debugs.task_activated[2],
"{{name}}", name)
task_str = string.gsub(task_str, "{{ts}}", ts)
util.debug(itte.config.debugs.task_activated[1], task_str,
itte.config.debug)
itte.handlers[task.handler](cxt, task)
-- Suspend coroutine to be reactivated later
coroutine.yield(co)
task.done = true
end
end
-- Initialise task coroutine
for name, task in pairs(cxt.tasks) do
task.co = coroutine.create(co)
task.done = false
util.debug(itte.config.debugs.task_added[1],
string.gsub(itte.config.debugs.task_added[2], "{{name}}", name),
itte.config.debug)
end
end
itte.docs.check_tasks = [[ (context_table)
Compare task times to the current time and resume a task's coroutine if
the task interval or time match the current time.
]]
function itte.check_tasks(cxt)
-- Exit function if there are no tasks for the context
if (cxt.tasks == nil) or (cxt.tasks == {}) then
do return end
end
local dt = util.split_str(os.date("%w %Y %m %d %H %M", os.time()))
-- Support preset minute intervals
local interval_min = { 5, 10, 15, 20, 30 }
for name, task in pairs(cxt.tasks) do
local task_min = string.sub(task.time, string.find(task.time, ":") + 1,
string.find(task.time, ":") + 2)
if (util.has_key(interval_min, tonumber(task.interval:sub(1, -2))) and
tonumber(dt[6]) % tonumber(task.interval:sub(1, -2)) == 0) or
(task.interval == "hourly" and task_min == dt[6]) or
(task.interval == "daily" and task.time == dt[5] .. ":" .. dt[6]) or
(task.interval == "weekly" and task.weekday == dt[1] and
task.time == dt[5] .. ":" .. dt[6]) or
(task.interval == "monthly" and task.day == dt[4] and
task.time == dt[5] .. ":" .. dt[6])
then
-- Run only once per interval
while not task.done do
coroutine.resume(task.co, name, task)
end
else
-- Reset the done flag after running task
task.done = false
end
end
end
-- ---------------------------------------------------------------------------
-- Runtime
-- ---------------------------------------------------------------------------
itte.docs.run = [[ () itte.docs.run = [[ ()
Run the client. Run the client.
]] ]]
@ -882,12 +976,19 @@ function itte.run()
itte.connect_server(name) itte.connect_server(name)
end end
while itte.state.connected do if itte.state.connected then
for _, cxt in pairs(itte.contexts) do for _, cxt in pairs(itte.contexts) do
itte.add_listener(cxt) itte.add_tasks(cxt)
end end
end end
while itte.state.connected do
for _, cxt in pairs(itte.contexts) do
itte.add_listener(cxt)
itte.check_tasks(cxt)
end
end
end end