Add scheduled tasks

trunk
mio 2022-04-01 08:14:15 +00:00
parent 8530a3bb77
commit e9927fb1d7
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
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
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
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")
containing selection tables and creates handlers to return responses, one

View File

@ -423,14 +423,22 @@ local h = {}
-- Serve ramen.
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
local roll = math.random(1, 100)
if roll == 77 then
irc.message(cxt, { msg.reply_to },
irc.message(cxt, recipients, intro ..
string.gsub(itte_config.messages.special, "{{ramen}}",
ramen.make_ramen("veg_special")))
else
irc.message(cxt, { msg.reply_to },
irc.message(cxt, recipients, intro ..
string.gsub(itte_config.messages.serve, "{{ramen}}",
ramen.make_ramen("veg")))
end
@ -443,12 +451,12 @@ function h.mramen(cxt, msg)
local roll = math.random(1, 100)
if roll == 77 then
irc.message(cxt, { msg.reply_to },
string.gsub(itte_config.messages.special, "{{ramen}}",
ramen.make_ramen("meat_special")))
string.gsub(itte_config.messages.special, "{{ramen}}",
ramen.make_ramen("meat_special")))
else
irc.message(cxt, { msg.reply_to },
string.gsub(itte_config.messages.serve, "{{ramen}}",
ramen.make_ramen("meat")))
string.gsub(itte_config.messages.serve, "{{ramen}}",
ramen.make_ramen("meat")))
end
end
@ -474,4 +482,10 @@ function h.water(cxt, msg)
end
-- Serve ramen at the interval specified in the config.
function h.th_ramen(cxt, task)
h.ramen(cxt, task)
end
itte_handlers = h

View File

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

117
itte.lua
View File

@ -24,6 +24,7 @@ itte.confs = {
itte.config = {
debug = false,
admin_handlers = { "connect", "quit", "reload", "servers", "join", "part" },
task_handler_prefix = "th_",
messages = {
connect_success = "Connected to {{server}}.",
help = "Service codes available: {{codes}}",
@ -56,6 +57,8 @@ itte.config = {
redact = { "********" },
send = { "send" },
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
itte.state = {
connected = false,
timeout = 0.1,
}
itte.contexts = {}
@ -143,16 +147,19 @@ function itte.get_config(reload)
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 name name has not changed between reloads.
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
cxt_old = itte.contexts[name]
itte.contexts[name] = prop
itte.contexts[name].name = name
itte.contexts[name].cmds = itte.get_commands(prop)
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
@ -307,7 +314,7 @@ function itte.init_socket(name, svr)
context.con = ssl.wrap(context.con, con_params)
context.con:dohandshake()
-- Set a short timeout to be non-blocking
context.con:settimeout(0.1)
context.con:settimeout(itte.state.timeout)
-- Set context and global states
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],
itte.config.debug)
itte.state.connected = false
os.exit()
end
end
@ -767,6 +775,12 @@ function itte._h.help(cxt, msg)
end
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, ", " ..
cxt.code_prefix)
-- 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)
@ -838,9 +852,12 @@ function itte.listen(name, str)
util.debug(itte.config.debugs.listen[1], str, itte.config.debug)
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
-- 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)
-- Check the handlers table
elseif util.has_key(itte.handlers, msg.code) then
@ -872,6 +889,83 @@ function itte.add_listener(cxt)
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 = [[ ()
Run the client.
]]
@ -882,12 +976,19 @@ function itte.run()
itte.connect_server(name)
end
while itte.state.connected do
if itte.state.connected then
for _, cxt in pairs(itte.contexts) do
itte.add_listener(cxt)
itte.add_tasks(cxt)
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