--- -- Implementation of the main LuaIRC module -- initialization {{{ local base = _G local constants = require 'irc.constants' local ctcp = require 'irc.ctcp' local c = ctcp._ctcp_quote local irc_debug = require 'irc.debug' local message = require 'irc.message' local misc = require 'irc.misc' local socket = require 'socket' local os = require 'os' local string = require 'string' local table = require 'table' -- }}} --- -- LuaIRC - IRC framework written in Lua -- @release 0.3 module 'irc' -- constants {{{ _VERSION = 'LuaIRC 0.3' -- }}} -- classes {{{ local Channel = base.require 'irc.channel' -- }}} -- local variables {{{ local irc_sock = nil local rsockets = {} local wsockets = {} local rcallbacks = {} local wcallbacks = {} local icallbacks = { whois = {}, serverversion = {}, servertime = {}, ctcp_ping = {}, ctcp_time = {}, ctcp_version = {}, } local requestinfo = {whois = {}} local handlers = {} local ctcp_handlers = {} local user_handlers = {} local serverinfo = {} local ip = nil -- }}} -- defaults {{{ TIMEOUT = 60 -- connection timeout NETWORK = "localhost" -- default network PORT = 6667 -- default port NICK = "luabot" -- default nick USERNAME = "LuaIRC" -- default username REALNAME = "LuaIRC" -- default realname DEBUG = false -- whether we want extra debug information OUTFILE = nil -- file to send debug output to - nil is stdout -- }}} -- private functions {{{ -- main_loop_iter {{{ local function main_loop_iter() if #rsockets == 0 and #wsockets == 0 then return false end local rready, wready, err = socket.select(rsockets, wsockets) if err then irc_debug._err(err); return false; end for _, sock in base.ipairs(rready) do local cb = socket.protect(rcallbacks[sock]) local ret, err = cb(sock) if not ret then irc_debug._warn("socket error: " .. err) _unregister_socket(sock, 'r') end end for _, sock in base.ipairs(wready) do local cb = socket.protect(wcallbacks[sock]) local ret, err = cb(sock) if not ret then irc_debug._warn("socket error: " .. err) _unregister_socket(sock, 'w') end end return true end -- }}} -- begin_main_loop {{{ local function begin_main_loop() while main_loop_iter() do end end -- }}} -- incoming_message {{{ local function incoming_message(sock) local raw_msg = socket.try(sock:receive()) irc_debug._message("RECV", raw_msg) local msg = message._parse(raw_msg) misc._try_call_warn("Unhandled server message: " .. msg.command, handlers["on_" .. msg.command:lower()], (misc._parse_user(msg.from)), base.unpack(msg.args)) return true end -- }}} -- callback {{{ local function callback(name, ...) return misc._try_call(user_handlers[name], ...) end -- }}} -- }}} -- internal message handlers {{{ -- command handlers {{{ -- on_nick {{{ function handlers.on_nick(from, new_nick) for chan in channels() do chan:_change_nick(from, new_nick) end callback("nick_change", new_nick, from) end -- }}} -- on_join {{{ function handlers.on_join(from, chan) base.assert(serverinfo.channels[chan], "Received join message for unknown channel: " .. chan) if serverinfo.channels[chan].join_complete then serverinfo.channels[chan]:_add_user(from) callback("join", serverinfo.channels[chan], from) end end -- }}} -- on_part {{{ function handlers.on_part(from, chan, part_msg) -- don't assert on chan here, since we get part messages for ourselves -- after we remove the channel from the channel list if not serverinfo.channels[chan] then return end if serverinfo.channels[chan].join_complete then serverinfo.channels[chan]:_remove_user(from) callback("part", serverinfo.channels[chan], from, part_msg) end end -- }}} -- on_mode {{{ function handlers.on_mode(from, to, mode_string, ...) local dir = mode_string:sub(1, 1) mode_string = mode_string:sub(2) local args = {...} if to:sub(1, 1) == "#" then -- handle channel mode requests {{{ base.assert(serverinfo.channels[to], "Received mode change for unknown channel: " .. to) local chan = serverinfo.channels[to] local ind = 1 for i = 1, mode_string:len() do local mode = mode_string:sub(i, i) local target = args[ind] -- channel modes other than op/voice will be implemented as -- information request commands if mode == "o" then -- channel op {{{ chan:_change_status(target, dir == "+", "o") callback(({["+"] = "op", ["-"] = "deop"})[dir], chan, from, target) ind = ind + 1 -- }}} elseif mode == "v" then -- voice {{{ chan:_change_status(target, dir == "+", "v") callback(({["+"] = "voice", ["-"] = "devoice"})[dir], chan, from, target) ind = ind + 1 -- }}} end end -- }}} elseif from == to then -- handle user mode requests {{{ -- TODO: make users more easily accessible so this is actually -- reasonably possible for i = 1, mode_string:len() do local mode = mode_string:sub(i, i) if mode == "i" then -- invisible {{{ -- }}} elseif mode == "s" then -- server messages {{{ -- }}} elseif mode == "w" then -- wallops messages {{{ -- }}} elseif mode == "o" then -- ircop {{{ -- }}} end end -- }}} end end -- }}} -- on_topic {{{ function handlers.on_topic(from, chan, new_topic) base.assert(serverinfo.channels[chan], "Received topic message for unknown channel: " .. chan) serverinfo.channels[chan]._topic.text = new_topic serverinfo.channels[chan]._topic.user = from serverinfo.channels[chan]._topic.time = os.time() if serverinfo.channels[chan].join_complete then callback("topic_change", serverinfo.channels[chan]) end end -- }}} -- on_invite {{{ function handlers.on_invite(from, to, chan) callback("invite", from, chan) end -- }}} -- on_kick {{{ function handlers.on_kick(from, chan, to) base.assert(serverinfo.channels[chan], "Received kick message for unknown channel: " .. chan) if serverinfo.channels[chan].join_complete then serverinfo.channels[chan]:_remove_user(to) callback("kick", serverinfo.channels[chan], to, from) end end -- }}} -- on_privmsg {{{ function handlers.on_privmsg(from, to, msg) local msgs = ctcp._ctcp_split(msg) for _, v in base.ipairs(msgs) do local msg = v.str if v.ctcp then -- ctcp message {{{ local words = misc._split(msg) local received_command = words[1] local cb = "on_" .. received_command:lower() table.remove(words, 1) -- not using try_call here because the ctcp specification requires -- an error response to nonexistant commands if base.type(ctcp_handlers[cb]) == "function" then ctcp_handlers[cb](from, to, table.concat(words, " ")) else notice(from, c("ERRMSG", received_command, ":Unknown query")) end -- }}} else -- normal message {{{ if to:sub(1, 1) == "#" then base.assert(serverinfo.channels[to], "Received channel msg from unknown channel: " .. to) callback("channel_msg", serverinfo.channels[to], from, msg) else callback("private_msg", from, msg) end -- }}} end end end -- }}} -- on_notice {{{ function handlers.on_notice(from, to, msg) local msgs = ctcp._ctcp_split(msg) for _, v in base.ipairs(msgs) do local msg = v.str if v.ctcp then -- ctcp message {{{ local words = misc._split(msg) local command = words[1]:lower() table.remove(words, 1) misc._try_call_warn("Unknown CTCP message: " .. command, ctcp_handlers["on_rpl_"..command], from, to, table.concat(words, ' ')) -- }}} else -- normal message {{{ if to:sub(1, 1) == "#" then base.assert(serverinfo.channels[to], "Received channel msg from unknown channel: " .. to) callback("channel_notice", serverinfo.channels[to], from, msg) else callback("private_notice", from, msg) end -- }}} end end end -- }}} -- on_quit {{{ function handlers.on_quit(from, quit_msg) for name, chan in base.pairs(serverinfo.channels) do chan:_remove_user(from) end callback("quit", from, quit_msg) end -- }}} -- on_ping {{{ -- respond to server pings to make sure it knows we are alive function handlers.on_ping(from, respond_to) send("PONG", respond_to) end -- }}} -- }}} -- server replies {{{ -- on_rpl_topic {{{ -- catch topic changes function handlers.on_rpl_topic(from, chan, topic) base.assert(serverinfo.channels[chan], "Received topic information about unknown channel: " .. chan) serverinfo.channels[chan]._topic.text = topic end -- }}} -- on_rpl_notopic {{{ function handlers.on_rpl_notopic(from, chan) base.assert(serverinfo.channels[chan], "Received topic information about unknown channel: " .. chan) serverinfo.channels[chan]._topic.text = "" end -- }}} -- on_rpl_topicdate {{{ -- "topic was set by at