local cqueues = require("cqueues") local socket = require("cqueues.socket") local errno = require("cqueues.errno") local loop = cqueues.new() local host, port, socketpath = ... -------------------------------------------------------------------------------- -- 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 = {} } 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 print("DEBUG: calling handler for:", cmd, prefix, table.unpack(params)) 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() print("DEBUG step:", ok, err) until not ok cqueues.poll(self) print("DEBUG: loop") until self.conn:eof() 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:send_chat(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, "^[^ ]+ (.*)") print("DEBUG: cmd:", cmd) print("DEBUG: data:", data) 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 --------------------------------------------------------------------------- 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() srv:loop(loop) end) ------------------------------------------------------------------------------------------- -- mainloop 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(socketpath)