about summary refs log tree commit diff
path: root/bot.lua
diff options
context:
space:
mode:
authorequa <equaa@protonmail.com>2022-03-07 19:14:15 +0000
committerequa <equaa@protonmail.com>2022-03-07 19:14:15 +0000
commit224e8f43c50a513bae78af2152c12c0a5f9564f9 (patch)
tree7748d6ffeba50a43d3a7f55a3c85983d07969776 /bot.lua
initial commit
Diffstat (limited to 'bot.lua')
-rw-r--r--bot.lua199
1 files changed, 199 insertions, 0 deletions
diff --git a/bot.lua b/bot.lua
new file mode 100644
index 0000000..f957fd3
--- /dev/null
+++ b/bot.lua
@@ -0,0 +1,199 @@
+local cqueues = require("cqueues")
+local socket = require("cqueues.socket")
+local condition = require("cqueues.condition")
+local std = require("posix.unistd")
+local stdio = require("posix.stdio")
+local dkjson = require("dkjson")
+
+local fs = require("fs")
+
+local fifo = require("fifo")
+
+function irc_dump(fifo, socket)
+	for data in fifo:iter() do
+		data = emit_message(data)
+		if not string.match(data, "[\r\n]") and #string <= 510 then
+			socket:write(data .. "\r\n")
+		end
+	end
+end
+
+function parse_message(data)
+	if string.match(data, "[\0\r\n]") then return nil, "illegal character" end
+
+	local pos = 1
+	local prefix
+	local command
+	local params = {}
+
+	if string.match(data, "^:") then
+		prefix, pos = string.match(data, "^:([^ ]+)()")
+	end
+
+	command, pos = string.match(data, "([^ ]+)()", pos)
+	while pos < #data do
+		local start, param
+		start, param, pos = string.match(data, "()([^ ]+)()", pos)
+
+		if not start then break end
+		if string.sub(param, 1, 1) == ":" then
+			table.insert(params, string.sub(data, start + 1))
+			break
+		else
+			table.insert(params, param)
+		end
+	end
+
+	return { params = params, command = command, prefix = prefix }
+end
+
+function emit_message(message)
+	local out = {}
+	if message.prefix then
+		table.insert(out, message.prefix)
+		table.insert(out, " ")
+	end
+
+	table.insert(out, message.command)
+
+	for i, v in ipairs(message.params or {}) do
+		assert(not string.match(i, "[\0\r\n]"))
+
+		if i == #(message.params or {}) then
+			table.insert(out, " :")
+			table.insert(out, v)
+		else
+			assert(not string.match(v, "^:"))
+			table.insert(out, " ")
+			table.insert(out, v)
+		end
+	end
+
+	return table.concat(out)
+end
+
+local irc_handlers = {}
+
+function handle_ed(loop, in_queue, out_queue, command)
+	local ed_out = { std.pipe() }
+	if not (ed_out[1] and ed_out[2]) then return nil, "can't create pipe" end
+	local ed_in = { std.pipe() }
+	if not (ed_in[1] and ed_in[2]) then return nil, "can't create pipe" end
+
+	local pid, err = std.fork()
+	if not pid then return nil, err end
+
+	if pid == 0 then
+		std.close(ed_out[1])
+		std.close(ed_in[2])
+		std.dup2(ed_in[1], std.STDIN_FILENO)
+		std.dup2(ed_out[2], std.STDOUT_FILENO)
+		std.execp(command[1], { select(2, table.unpack(command)) })
+		os.exit(100)
+	else
+		loop:wrap(function ()
+			local out = fs.new(ed_in[2], "w")
+			while true do
+				local x = fifo.get(in_queue)
+				print("ed: got " .. x)
+				out:write(x .. "\n")
+			end
+		end)
+		loop:wrap(function ()
+			local inp = fs.new(ed_out[1])
+			for l in inp:lines() do
+				fifo.put(out_queue, l)
+			end
+		end)
+	end
+end
+
+function irc_handlers.PING(state, line)
+	fifo.put(state.queue, { command = "PONG", params = line.params })
+end
+
+function irc_handlers._001(state, line)
+	state.nick = line.params[1]
+	fifo.put(state.queue, { command = "JOIN", params = { state.channel } })
+end
+
+function irc_handlers.PRIVMSG(state, line)
+	-- fifo.put(state.queue, { command = "NOTICE", params = {
+	-- 	state.channel,
+	-- 	"A! " .. line.params[2]
+	-- } })
+	fifo.put(state.to_ed, line.params[2])
+end
+function irc_connect(loop, host, config)
+	local state = {
+		queue = fifo.new(),
+		nick = config.nick or "ed1bot",
+	}
+
+	-- populate channels
+	-- TODO good
+	for k in pairs(config.channels or { ["#ed1bot"] = {} }) do
+		state.channel = k
+		break
+	end
+
+	local sock = assert(socket.connect(host))
+	sock:starttls()
+
+	loop:wrap(function () irc_dump(state.queue, sock) end)
+	loop:wrap(function ()
+		for raw_line in sock:lines("*l") do -- TODO set line length
+			local line = parse_message(raw_line)
+			-- TODO assert line or something
+			if not line then
+				print("bad line! " .. raw_line)
+				line = { command = "BADLINE", params = { line } }
+			end
+
+			print(dkjson.encode(parse_message(raw_line)))
+
+			local command = (tonumber(line.command) and "_" or "") .. line.command
+			if irc_handlers[command] then
+				irc_handlers[command](state, line)
+			end
+		end
+
+		os.exit(0)
+	end)
+
+	loop:wrap(function ()
+		local f = fs.new(0)
+		for line in f:lines() do
+			print(line)
+			fifo.put(state.queue, parse_message(line))
+		end
+	end)
+
+	state.to_ed, state.from_ed = fifo.new(), fifo.new()
+	handle_ed(loop, state.to_ed, state.from_ed, { "red" })
+
+	loop:wrap(function()
+		while true do
+			local line = fifo.get(state.from_ed)
+			fifo.put(state.queue, {
+				command = "PRIVMSG",
+				params = {
+					state.channel,
+					line
+				}
+			})
+		end
+	end)
+
+	fifo.put(state.queue, { command = "NICK", params = { "ed1bot" } })
+	fifo.put(state.queue, { command = "USER", params = { "ed1bot", "0", "*", ":)" } })
+end
+
+local main = cqueues.new()
+if not arg[1] then
+	print("usage: edbot [CONFIG]")
+	os.exit(1)
+end
+main:wrap(function () irc_connect(main, { host = "localhost", port = 6697 }, dofile(arg[1])) end)
+assert(main:loop())
+return { parse_message = parse_message }