local cqueues = require("cqueues") local socket = require("cqueues.socket") local errno = require("cqueues.errno") local condition = require("cqueues.condition") local loop = cqueues.new() local host, port, socketpath = ... -------------------------------------------------------------------------------- -- fifo queue local fifo = {} function fifo.new() local list = {} list.next = list list.prev = list return setmetatable({list = list, condvar = condition.new()}, {__index = fifo}) end function fifo:enqueue(data) local node = { data = data} node.next = self.list.next node.prev = self.list self.list.next.prev = node self.list.next = node self:signal() return self end function fifo:dequeue() local node = self.list.prev self.list.prev = node.prev self.list.prev.next = node.next return node.data end function fifo:empty() return self.list == self.list.next end function fifo:signal() self.condvar:signal() end function fifo:getcv() return self.condvar end -------------------------------------------------------------------------------- -- irc session local irc = { callbacks = {} } function irc.new(nick, username, password, realname) local self = { nick = nick or "albotty", username = username or "albotty", password = password, realname = realname or "albotty", hooks = {}, channels = {}, msgq = fifo.new() } return setmetatable(self, { __index = irc }) end function irc:autojoin(channels) for _,chan in pairs(channels) do self.channels[chan] = self.channels[chan] or {} self.channels[chan].autojoin = true self.channels[chan].joined = false end end function irc:connect(host, port) local hostname, password, tls, timeout if type(host) == "table" then hostname = host.host port = host.port password = host.password tls = host.tls timeout = host.timeout else hostname = host end self.conn = socket.connect(hostname, port) self.conn:settimeout(timeout or 10) self:send("NICK %s", self.nick) self:send("USER %s 0 * :%s", self.username, self.realname) return self end function irc:pollfd() return self.conn:pollfd() end function irc:events() return self.conn:events() end function irc:send(msg, ...) if select("#", ...) > 0 then msg = msg:format(...) end self:run_hooks("OnSend", msg) self.conn:write(msg.."\r\n") self.conn:flush() end function irc:send_chat(target, message) for line in message:gmatch("([^\r\n]+)") do self:send("PRIVMSG %s :%s", target, line) end end function irc:join(channel) self.channels[channel] = self.channels[channel] or {} self.channels[channel].joined = true self:send("JOIN %s", channel) end function irc:hook(event, id, func) func = func or id self.hooks[event] = self.hooks[event] or {} self.hooks[event][id] = func return self end function irc:run_hooks(event, ...) for id, func in pairs(self.hooks[event] or {}) do if func(self, ...) then return true end end end local function irc_parse(line) local prefix local lineStart = 1 if line:sub(1,1) == ":" then local space = line:find(" ") prefix = line:sub(2, space-1) lineStart = space end local _, trailToken = line:find("%s+:", lineStart) local lineStop = line:len() local trailing if trailToken then trailing = line:sub(trailToken + 1) lineStop = trailToken - 2 end local params = {} local _, cmdEnd, cmd = line:find("(%S+)", lineStart) local pos = cmdEnd + 1 while true do local _, stop, param = line:find("(%S+)", pos) if not param or stop > lineStop then break end pos = stop + 1 params[#params + 1] = param end if trailing then params[#params + 1] = trailing end return prefix, cmd, params end local function parse_prefix(prefix) local user = {} if prefix then user.access, user.nick, user.username, user.host = prefix:match("^([%+@]*)(.+)!(.+)@(.+)$") end return user end irc.callbacks["001"] = function(self, prefix, me) self.authed = true self.nick = me for name, chan in pairs(self.channels) do if chan.autojoin then self:join(name) end end end irc.callbacks["NICK"] = function(self, prefix, newnick) local user = parse_prefix(prefix) if self.nick == user.nick then self.nick = newnick end end irc.callbacks["NICK"] = function(self, prefix, newnick) local user = parse_prefix(prefix) if self.nick == user.nick then self.nick = newnick end self:run_hooks("OnNick", user, newnick) end irc.callbacks["PING"] = function(self, prefix, query) self:send("PONG :%s", query) end irc.callbacks["PRIVMSG"] = function(self, prefix, channel, message) self:run_hooks("OnChat", parse_prefix(prefix), channel, message) end irc.callbacks["NOTICE"] = function(self, prefix, channel, message) self:run_hooks("OnNotice", parse_prefix(prefix), channel, message) end irc.callbacks["JOIN"] = function(self, prefix, channel) self:run_hooks("OnJoin", parse_prefix(prefix), channel) end irc.callbacks["PART"] = function(self, prefix, channel, reason) local user = parse_prefix(prefix) self:run_hooks("OnPart", user, channel, reason) end irc.callbacks["KICK"] = function(self, prefix, channel, kicked, reason) self:run_hooks("OnKick", channel, kicked, parse_prefix(prefix), reason) end irc.callbacks["QUIT"] = function(self, prefix, message) local user = parse_prefix(prefix) self:run_hooks("OnQuit", user, message) end -- nick in use irc.callbacks["432"] = function(self, prefix, target, badnick) local n,i = string.match(self.nick, "(.*)(%d+)$") local newnick newnick = string.format("%s%d",n or self.nick, i+1) self:send("NICK %s", newnick) end irc.callbacks["433"] = irc.callbacks["422"] -- no topic irc.callbacks["331"] = function(self, prefix, me, channel) self:run_hooks("OnTopic", channel, nil) end irc.callbacks["332"] = function(self, prefix, me, channel, topic) self:run_hooks("OnTopic", channel, topic) end irc.callbacks["333"] = function(self, prefix, me, channel, nick, time) self:run_hooks("OnTopicInfo", channel, nick, tonumber(time)) end irc.callbacks["TOPIC"] = function(self, prefix, channel, topic) self:run_hooks("OnTopic", channel, topic) end -- RPL_UMODEIS irc.callbacks["221"] = function(self, prefix, user, modes) self:run_hooks("OnUserMode", modes) end -- RPL_CHANNELMODEIS irc.callbacks["324"] = function(self, prefix, channel, modes) self:run_hooks("OnChannelMode", channel, modes) end irc.callbacks["MODE"] = function(self, prefix, target, modes, ...) self:run_hooks("OnModeChange", parse_prefix(prefix), target, modes, ...) end function irc:handle(line) local prefix, cmd, params = irc_parse(line) callback = self.callbacks[cmd] if type(callback) == "function" then return callback(self, prefix, table.unpack(params)) else print("DEBUG: no handler for:", cmd, "prefix:",prefix) end end function irc:step() local line, why = self.conn:recv("*L") if not line or #line == 0 then return nil, why end self:handle(line) return line end function irc:loop() repeat repeat local ok, err = self:step() until not ok cqueues.poll(self) until self.conn:eof() end function irc:msgq_loop() while true do self.msgq:getcv():wait() local loop = cqueues.new() loop:wrap(function() while not self.msgq:empty() do local m = self.msgq:dequeue() self:send_chat(m.target, m.message) cqueues.sleep(1.0) end end) loop:loop() end end function irc:enqueue(target, msg) self.msgq:enqueue{target=target, message =msg} end -------------------------------------------------------------------------- -- sockserver local sockserver = { callbacks = {}} function sockserver.new(ircsession, socketpath, timeout) local path = socketpath or "/tmp/albotty.sock" local self = { path = path, ircsess = ircsession, socket = socket.listen{path=path}, timeout = timeout, } return setmetatable(self, { __index = sockserver }) end sockserver.callbacks["/msg"] = function(self, data) local dest,msg = string.match(data or "", "^([^ ]+) (.*)") if dest then self.ircsess:enqueue(dest, msg) return "ok" end return "error: msg format" end sockserver.callbacks["bye"] = function(serf, data) return "bye!" end function sockserver:run_callback(line) local cmd = string.match(line, "^([^ ]+)") local data = string.match(line, "^[^ ]+ (.*)") if not cmd or type(self.callbacks[cmd]) ~= "function" then return nil end return cmd, self.callbacks[cmd](self, data) end function sockserver:loop(loop) for conn in self.socket:clients(self.timeout) do loop:wrap(function() for line in conn:lines("*l") do print("DEBUG: got line:", line) local cmd, resp = self:run_callback(line) if not cmd then conn:write("error: unknown command\n") end if resp then conn:write(resp.."\n") end if cmd == "bye" then conn:shutdown("w") break end end end) end end --------------------------------------------------------------------------- -- main sess = irc.new() sess:hook("OnChat", function(self, user, channel, message) print("PRIVMSG:") print(" nick:", user.nick) print(" channel:", channel) print(" message:", message) if channel == self.nick then self:send_chat(user.nick, "Hi there") else self:send_chat(channel, "Hi "..user.nick) end end) sess:hook("OnJoin", function(self, user, channel) print("JOIN:") print(" nick:", user.nick) print(" channel:", channel) end) sess:hook("OnKick", function(self, channel, kicked, user, reason) print("Kicked:") print((" %s kicked %s from %s"):format(user.nick, kicked, channel)) if kicked == self.nick then self:join(channel) end end) sess:autojoin{"#alpine-test", "#alpine"} sess:connect("172.16.3.54", "6667") print("CONNECTED!") local srv = sockserver.new(sess) loop:wrap(function() sess:loop() end) loop:wrap(function() sess:msgq_loop() end) loop:wrap(function() srv:loop(loop) end) while not loop:empty() do local ok, err = loop:step() print("DEBUG: mainloop") if not ok then os.remove(srv.path) error("loop.step: "..err) end end os.remove(srv.path)