about summary refs log tree commit diff
path: root/bot.lua
blob: ac0a10c0ed8abe27ad2e0d081c75098877a1a589 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
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")
local mom = require("mom")

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",
		command = config.command,
	}

	-- 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" })
	state.mom = mom.new(loop)
	state.to_ed, state.from_ed = select(2, state.mom:create(state.command))

	loop:wrap(function()
		while true do
			local line = fifo.get(state.from_ed)
			if type(line) == "string" then
				fifo.put(state.queue, {
					command = "PRIVMSG",
					params = {
						state.channel,
						line
					}
				})
			else -- TODO other possible types
				fifo.put(state.queue, {
					command = "PART",
					params = {
						state.channel
					},
				})
				-- be annoying
				cqueues.poll(10.0)
				fifo.put(state.queue, {
					command = "JOIN",
					params = {
						state.channel
					},
				})
			end
		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 }