about summary refs log tree commit diff
diff options
context:
space:
mode:
authorequa <equaa@protonmail.com>2022-03-13 18:01:02 +0000
committerequa <equaa@protonmail.com>2022-03-13 18:10:04 +0000
commit2d581a2b62a398963e4e418243b10e02b8d648af (patch)
tree992c7352f2451a50e4c6cb20dfdfc5262bc0580e
parent199b82b6767dcbe0a3e5cd770402d87e1be04f31 (diff)
Various fixes
- add directory configuration option
- add debug mode for checking output
- fix FIFOs not actually being FIFOs
- add user and pass configuration options
- validate config files
- don't fail instantly if we fail to exec or chdir; instead, wait for input
  to avoid restart loops
- have ed restart on JOIN rather than instantly
- output stderr to the channel as well
-rw-r--r--TODO1
-rw-r--r--bot.lua68
-rw-r--r--config.example.lua13
-rw-r--r--fifo.lua10
-rw-r--r--mom.lua22
5 files changed, 98 insertions, 16 deletions
diff --git a/TODO b/TODO
index 673b2fa..028c7b4 100644
--- a/TODO
+++ b/TODO
@@ -1,4 +1,3 @@
-have the shell cd for us (we can do this by running sh -c, but making it cd would be better)
 support joining irc channels with keys
 support empty lines
 throttle really long messages
diff --git a/bot.lua b/bot.lua
index db01ba8..0704b63 100644
--- a/bot.lua
+++ b/bot.lua
@@ -7,7 +7,9 @@ local fifo = require("fifo")
 local mom = require("mom")
 
 local function parse_message(data)
-	if string.match(data, "[\0\r\n]") then return nil, "illegal character" end
+	if string.match(data, "[\0\r\n]") then
+		return nil, "illegal character"
+	end
 
 	local pos = 1
 	local prefix
@@ -108,16 +110,23 @@ function irc_handlers.JOIN(state, line)
 	if not state.channels[line.params[1]] then
 		-- TODO validate the params at all
 		local ch_name = line.params[1]
+		local ch_config = config.channels[ch_name]
 		local ch = {}
 		state.channels[ch_name] = ch
 
-		ch.command = config.channels[ch_name].command or config.command
-		ch.invoke = config.channels[ch_name].invoke or config.invoke
+		ch.command = ch_config.command or config.command
+		ch.invoke = ch_config.invoke or config.invoke
+		ch.directory = ch_config.directory or config.directory
 
 		ch.mom_id, ch.tx_queue = state.mom:create(
 			ch.command,
 			ch_name,
-			state.rx_queue
+			state.rx_queue,
+			ch.directory
+		)
+	else
+		state.mom:start_process(
+			state.channels[line.params[1]].mom_id
 		)
 	end
 end
@@ -134,6 +143,7 @@ function irc_handlers.PRIVMSG(state, line)
 	local full_line = line.params[2]
 	local line
 	for _, prefix in ipairs(state.channels[ch].invoke) do
+		-- TODO: escape the nick itself
 		prefix = string.gsub(prefix, "%%n", state.nick)
 		prefix = string.gsub(prefix, "%%%", "%")
 		-- TODO parens and stuff maybe
@@ -199,8 +209,10 @@ local function irc_connect(loop, config)
 		-- a channel has the fields:
 		-- - invoke, a list of patterns (as in config, but being
 		--   properly populated with defaults)
+		-- - command, the command to run as a list
+		-- - directory, the dir to run it in
 		-- - mom_id, the id in the state's process mom
-		-- - tx_queue: to send messages to the ed queue
+		-- - tx_queue, to send messages to the ed queue
 		--   (note we don't have rx_queue: that is shared)
 		channels = {},
 	}
@@ -220,6 +232,10 @@ local function irc_connect(loop, config)
 				line = { command = "BADLINE", params = { line } }
 			end
 
+			if line.command == "ERROR" or state.config.debug then
+				print(raw_line)
+			end
+
 			local command = (tonumber(line.command) and "_" or "") .. line.command
 			if irc_handlers[command] then
 				irc_handlers[command](state, line)
@@ -239,14 +255,53 @@ local function irc_connect(loop, config)
 	state.mom = mom.new(loop)
 
 	loop:wrap(function () handle_ed_rx(state) end)
+
+	if config.pass then
+		fifo.put(state.queue, {
+			command = "PASS", params = { config.pass }
+		})
+	end
+
 	fifo.put(state.queue, { command = "NICK", params = { config.nick } })
 	-- TODO: config
 	fifo.put(state.queue, {
 		command = "USER",
-		params = { "ed1bot", "0", "*", ":)" },
+		params = { config.user, "0", "*", ":)" },
 	})
 end
 
+local function validate_config(c)
+	local function valid(name, thing, kind)
+		assert(type(thing) == kind,
+			kind .. " " .. name .. " must be specified"
+		)
+	end
+
+	local function optional(name, thing, kind)
+		if thing then valid(name, thing, kind) end
+	end
+
+	valid("host", c.host, "table")
+	valid("host.host", c.host.host, "string")
+	valid("host.port", c.host.port, "number")
+	valid("nick", c.nick, "string")
+	valid("user", c.user, "string")
+	optional("pass", c.pass, "string")
+	valid("directory", c.directory, "string")
+	-- TODO better
+	valid("command", c.command, "table")
+	valid("invoke", c.invoke, "table")
+	valid("channels", c.channels, "table")
+
+	for k, v in pairs(c.channels) do
+		local name = string.format("channels[%q]", k)
+		valid(name, v, "table")
+		optional(name .. ".invoke", v.invoke, "table")
+		optional(name .. ".command", v.command, "table")
+		optional(name .. ".directory", v.directory, "string")
+	end
+end
+
 do
 	local main = cqueues.new()
 	if not arg[1] then
@@ -255,6 +310,7 @@ do
 	end
 
 	local config = assert(dofile(arg[1]))
+	validate_config(config)
 
 	main:wrap(function ()
 		irc_connect(main, config)
diff --git a/config.example.lua b/config.example.lua
index 1d65b8c..faafcf4 100644
--- a/config.example.lua
+++ b/config.example.lua
@@ -4,9 +4,18 @@ return {
 		port = 6667,
 		tls = false,
 	},
-	nick = "ed1bot",
-	-- allow dms?
+	-- print all messages received from the server to stdout
+	debug = true,
+	nick = "edward",
+	user = "edward",
+	-- pass = "user:pass"
+	-- allow dms? currently unimplemented
 	direct = false,
+	-- directory to run commands under. this can also be specified
+	-- in the individual channels; the individual settings overwrite
+	-- this one and don't act relative to it.
+	directory = "testbed",
+	-- this one can be overwritten too
 	command = { "red" },
 	-- a ist of lua patterns (maybe regex later) that
 	-- tells edbot when to listen for commands
diff --git a/fifo.lua b/fifo.lua
index 03a7af7..f3a3d10 100644
--- a/fifo.lua
+++ b/fifo.lua
@@ -11,7 +11,7 @@ function fifo.signal(f)
 end
 
 function fifo.get(f)
-	if not f.head then
+	while not f.head do
 		f.cond:wait()
 	end
 
@@ -21,7 +21,13 @@ function fifo.get(f)
 end
 
 function fifo.put(f, data)
-	f.head = { data = data, tail = f.head }
+	local tail = f.head
+	while tail and tail.tail do tail = tail.tail end
+	if tail then
+		tail.tail = { data = data }
+	else
+		f.head = { data = data }
+	end
 	fifo.signal(f)
 end
 
diff --git a/mom.lua b/mom.lua
index ef0f4c8..3cc5696 100644
--- a/mom.lua
+++ b/mom.lua
@@ -41,11 +41,12 @@ end
 -- { IDENTIFIER, "line", "meow" }
 -- this is to make multiplexing easier
 -- you must also give it a queue to send to (the rx_queue)
-function mom.create(m, command, rx_id, rx_queue)
+function mom.create(m, command, rx_id, rx_queue, directory)
 	local id = {}
 
 	m.clients[id] = {
 		command = assert(command),
+		directory = assert(directory),
 		rx_id = assert(rx_id),
 		tx_queue = fifo.new(),
 		rx_queue = assert(rx_queue),
@@ -59,6 +60,13 @@ function mom.create(m, command, rx_id, rx_queue)
 	return id, m.clients[id].tx_queue, m.clients[id].rx_queue
 end
 
+local function error_wait(err)
+	print("error: " .. err)
+	print("say something to try again")
+	io.read("*l")
+	os.exit(100)
+end
+
 function mom.start_process(m, id)
 	local proc_out_rx, proc_out_tx = std.pipe()
 	if not (proc_out_rx and proc_out_tx) then return nil, "can't create pipe" end
@@ -73,8 +81,14 @@ function mom.start_process(m, id)
 		std.close(proc_in_tx)
 		std.dup2(proc_in_rx, std.STDIN_FILENO)
 		std.dup2(proc_out_tx, std.STDOUT_FILENO)
-		std.execp(m.clients[id].command[1], { select(2, table.unpack(m.clients[id].command)) }) -- meh
-		os.exit(100) -- TODO
+		std.dup2(proc_out_tx, std.STDERR_FILENO)
+		local ok, err = std.chdir(m.clients[id].directory)
+		if not ok then
+			error_wait(err)
+		end
+
+		local _, err = std.execp(m.clients[id].command[1], { select(2, table.unpack(m.clients[id].command)) }) -- meh
+		error_wait(err)
 	else
 		m.pids[pid] = id
 		std.close(proc_in_rx)
@@ -128,8 +142,6 @@ function mom.tend(m)
 		if pid and status ~= "running" then
 			local client_id = m.pids[pid]
 			m.pids[pid] = nil
-			-- TODO: only restart sometimes
-			mom.start_process(m, client_id)
 			m.clients[client_id].rx_queue:put({ "quit" })
 		end
 	end