2 -- Implementation of the main LuaIRC module
6 local constants = require 'irc.constants'
7 local ctcp = require 'irc.ctcp'
8 local c = ctcp._ctcp_quote
9 local irc_debug = require 'irc.debug'
10 local message = require 'irc.message'
11 local misc = require 'irc.misc'
12 local socket = require 'socket'
13 local os = require 'os'
14 local string = require 'string'
15 local table = require 'table'
19 -- LuaIRC - IRC framework written in Lua
24 _VERSION = 'LuaIRC 0.2'
28 local Channel = base.require 'irc.channel'
31 -- local variables {{{
45 local requestinfo = {whois = {}}
47 local ctcp_handlers = {}
53 TIMEOUT = 60 -- connection timeout
54 NETWORK = "localhost" -- default network
55 PORT = 6667 -- default port
56 NICK = "luabot" -- default nick
57 USERNAME = "LuaIRC" -- default username
58 REALNAME = "LuaIRC" -- default realname
59 DEBUG = false -- whether we want extra debug information
60 OUTFILE = nil -- file to send debug output to - nil is stdout
63 -- private functions {{{
65 local function main_loop_iter()
66 if #rsockets == 0 and #wsockets == 0 then return false end
67 local rready, wready, err = socket.select(rsockets, wsockets)
68 if err then irc_debug._err(err); return false; end
70 for _, sock in base.ipairs(rready) do
71 local cb = socket.protect(rcallbacks[sock])
72 local ret, err = cb(sock)
74 irc_debug._warn("socket error: " .. err)
75 _unregister_socket(sock, 'r')
79 for _, sock in base.ipairs(wready) do
80 local cb = socket.protect(wcallbacks[sock])
81 local ret, err = cb(sock)
83 irc_debug._warn("socket error: " .. err)
84 _unregister_socket(sock, 'w')
92 -- begin_main_loop {{{
93 local function begin_main_loop()
94 while main_loop_iter() do end
98 -- incoming_message {{{
99 local function incoming_message(sock)
100 local raw_msg = socket.try(sock:receive())
101 irc_debug._message("RECV", raw_msg)
102 local msg = message._parse(raw_msg)
103 misc._try_call_warn("Unhandled server message: " .. msg.command,
104 handlers["on_" .. msg.command:lower()],
105 (misc.parse_user(msg.from)), base.unpack(msg.args))
111 -- internal message handlers {{{
112 -- command handlers {{{
114 function handlers.on_nick(from, new_nick)
115 for chan in channels() do
116 chan:_change_nick(from, new_nick)
118 misc._try_call(on_nick_change, new_nick, from)
123 function handlers.on_join(from, chan)
124 base.assert(serverinfo.channels[chan],
125 "Received join message for unknown channel: " .. chan)
126 if serverinfo.channels[chan].join_complete then
127 serverinfo.channels[chan]:_add_user(from)
128 misc._try_call(on_join, serverinfo.channels[chan], from)
134 function handlers.on_part(from, chan, part_msg)
135 -- don't assert on chan here, since we get part messages for ourselves
136 -- after we remove the channel from the channel list
137 if not serverinfo.channels[chan] then return end
138 if serverinfo.channels[chan].join_complete then
139 serverinfo.channels[chan]:_remove_user(from)
140 misc._try_call(on_part, serverinfo.channels[chan], from, part_msg)
146 function handlers.on_mode(from, to, mode_string, ...)
147 local dir = mode_string:sub(1, 1)
148 mode_string = mode_string:sub(2)
151 if to:sub(1, 1) == "#" then
152 -- handle channel mode requests {{{
153 base.assert(serverinfo.channels[to],
154 "Received mode change for unknown channel: " .. to)
155 local chan = serverinfo.channels[to]
157 for i = 1, mode_string:len() do
158 local mode = mode_string:sub(i, i)
159 local target = args[ind]
160 -- channel modes other than op/voice will be implemented as
161 -- information request commands
162 if mode == "o" then -- channel op {{{
163 chan:_change_status(target, dir == "+", "o")
164 misc._try_call(({["+"] = on_op, ["-"] = on_deop})[dir],
168 elseif mode == "v" then -- voice {{{
169 chan:_change_status(target, dir == "+", "v")
170 misc._try_call(({["+"] = on_voice, ["-"] = on_devoice})[dir],
177 elseif from == to then
178 -- handle user mode requests {{{
179 -- TODO: make users more easily accessible so this is actually
180 -- reasonably possible
181 for i = 1, mode_string:len() do
182 local mode = mode_string:sub(i, i)
183 if mode == "i" then -- invisible {{{
185 elseif mode == "s" then -- server messages {{{
187 elseif mode == "w" then -- wallops messages {{{
189 elseif mode == "o" then -- ircop {{{
199 function handlers.on_topic(from, chan, new_topic)
200 base.assert(serverinfo.channels[chan],
201 "Received topic message for unknown channel: " .. chan)
202 serverinfo.channels[chan]._topic.text = new_topic
203 serverinfo.channels[chan]._topic.user = (misc.parse_user(from))
204 serverinfo.channels[chan]._topic.time = os.time()
205 if serverinfo.channels[chan].join_complete then
206 misc._try_call(on_topic_change, serverinfo.channels[chan])
212 function handlers.on_invite(from, to, chan)
213 misc._try_call(on_invite, from, chan)
218 function handlers.on_kick(from, chan, to)
219 base.assert(serverinfo.channels[chan],
220 "Received kick message for unknown channel: " .. chan)
221 if serverinfo.channels[chan].join_complete then
222 serverinfo.channels[chan]:_remove_user(to)
223 misc._try_call(on_kick, serverinfo.channels[chan], to, from)
229 function handlers.on_privmsg(from, to, msg)
230 local msgs = ctcp._ctcp_split(msg)
231 for _, v in base.ipairs(msgs) do
235 local words = misc._split(msg)
236 local received_command = words[1]
237 local cb = "on_" .. received_command:lower()
238 table.remove(words, 1)
239 -- not using try_call here because the ctcp specification requires
240 -- an error response to nonexistant commands
241 if base.type(ctcp_handlers[cb]) == "function" then
242 ctcp_handlers[cb](from, to, table.concat(words, " "))
244 notice(from, c("ERRMSG", "Unknown query: " .. received_command))
248 -- normal message {{{
249 if to:sub(1, 1) == "#" then
250 base.assert(serverinfo.channels[to],
251 "Received channel msg from unknown channel: " .. to)
252 misc._try_call(on_channel_msg, serverinfo.channels[to], from,
255 misc._try_call(on_private_msg, from, msg)
264 function handlers.on_notice(from, to, msg)
265 local msgs = ctcp._ctcp_split(msg)
266 for _, v in base.ipairs(msgs) do
270 local words = misc._split(msg)
271 local command = words[1]:lower()
272 table.remove(words, 1)
273 misc._try_call_warn("Unknown CTCP message: " .. command,
274 ctcp_handlers["on_rpl_"..command], from, to,
275 table.concat(words, ' '))
278 -- normal message {{{
279 if to:sub(1, 1) == "#" then
280 base.assert(serverinfo.channels[to],
281 "Received channel msg from unknown channel: " .. to)
282 misc._try_call(on_channel_notice, serverinfo.channels[to],
285 misc._try_call(on_private_notice, from, msg)
294 function handlers.on_quit(from, quit_msg)
295 for name, chan in base.pairs(serverinfo.channels) do
296 chan:_remove_user(from)
298 misc._try_call(on_quit, from, quit_msg)
303 -- respond to server pings to make sure it knows we are alive
304 function handlers.on_ping(from, respond_to)
305 send("PONG", respond_to)
310 -- server replies {{{
312 -- catch topic changes
313 function handlers.on_rpl_topic(from, chan, topic)
314 base.assert(serverinfo.channels[chan],
315 "Received topic information about unknown channel: " .. chan)
316 serverinfo.channels[chan]._topic.text = topic
320 -- on_rpl_notopic {{{
321 function handlers.on_rpl_notopic(from, chan)
322 base.assert(serverinfo.channels[chan],
323 "Received topic information about unknown channel: " .. chan)
324 serverinfo.channels[chan]._topic.text = ""
328 -- on_rpl_topicdate {{{
329 -- "topic was set by <user> at <time>"
330 function handlers.on_rpl_topicdate(from, chan, user, time)
331 base.assert(serverinfo.channels[chan],
332 "Received topic information about unknown channel: " .. chan)
333 serverinfo.channels[chan]._topic.user = user
334 serverinfo.channels[chan]._topic.time = base.tonumber(time)
338 -- on_rpl_namreply {{{
339 -- handles a NAMES reply
340 function handlers.on_rpl_namreply(from, chanmode, chan, userlist)
341 base.assert(serverinfo.channels[chan],
342 "Received user information about unknown channel: " .. chan)
343 serverinfo.channels[chan]._chanmode = constants.chanmodes[chanmode]
344 local users = misc._split(userlist)
345 for k,v in base.ipairs(users) do
346 if v:sub(1, 1) == "@" or v:sub(1, 1) == "+" then
347 local nick = v:sub(2)
348 serverinfo.channels[chan]:_add_user(nick, v:sub(1, 1))
350 serverinfo.channels[chan]:_add_user(v)
356 -- on_rpl_endofnames {{{
357 -- when we get this message, the channel join has completed, so call the
359 function handlers.on_rpl_endofnames(from, chan)
360 base.assert(serverinfo.channels[chan],
361 "Received user information about unknown channel: " .. chan)
362 if not serverinfo.channels[chan].join_complete then
363 misc._try_call(on_me_join, serverinfo.channels[chan])
364 serverinfo.channels[chan].join_complete = true
369 -- on_rpl_welcome {{{
370 function handlers.on_rpl_welcome(from)
379 -- on_rpl_yourhost {{{
380 function handlers.on_rpl_yourhost(from, msg)
381 serverinfo.host = from
385 -- on_rpl_motdstart {{{
386 function handlers.on_rpl_motdstart(from)
392 function handlers.on_rpl_motd(from, motd)
393 serverinfo.motd = (serverinfo.motd or "") .. motd .. "\n"
397 -- on_rpl_endofmotd {{{
398 function handlers.on_rpl_endofmotd(from)
399 if not serverinfo.connected then
400 serverinfo.connected = true
401 serverinfo.connecting = false
402 misc._try_call(on_connect)
407 -- on_rpl_whoisuser {{{
408 function handlers.on_rpl_whoisuser(from, nick, user, host, star, realname)
409 local lnick = nick:lower()
410 requestinfo.whois[lnick].nick = nick
411 requestinfo.whois[lnick].user = user
412 requestinfo.whois[lnick].host = host
413 requestinfo.whois[lnick].realname = realname
417 -- on_rpl_whoischannels {{{
418 function handlers.on_rpl_whoischannels(from, nick, channel_list)
420 if not requestinfo.whois[nick].channels then
421 requestinfo.whois[nick].channels = {}
423 for _, channel in base.ipairs(misc._split(channel_list)) do
424 table.insert(requestinfo.whois[nick].channels, channel)
429 -- on_rpl_whoisserver {{{
430 function handlers.on_rpl_whoisserver(from, nick, server, serverinfo)
432 requestinfo.whois[nick].server = server
433 requestinfo.whois[nick].serverinfo = serverinfo
438 function handlers.on_rpl_away(from, nick, away_msg)
440 if requestinfo.whois[nick] then
441 requestinfo.whois[nick].away_msg = away_msg
446 -- on_rpl_whoisoperator {{{
447 function handlers.on_rpl_whoisoperator(from, nick)
448 requestinfo.whois[nick:lower()].is_oper = true
452 -- on_rpl_whoisidle {{{
453 function handlers.on_rpl_whoisidle(from, nick, idle_seconds)
454 requestinfo.whois[nick:lower()].idle_time = idle_seconds
458 -- on_rpl_endofwhois {{{
459 function handlers.on_rpl_endofwhois(from, nick)
461 local cb = table.remove(icallbacks.whois[nick], 1)
462 cb(requestinfo.whois[nick])
463 requestinfo.whois[nick] = nil
464 if #icallbacks.whois[nick] > 0 then send("WHOIS", nick)
465 else icallbacks.whois[nick] = nil
470 -- on_rpl_version {{{
471 function handlers.on_rpl_version(from, version, server, comments)
472 local cb = table.remove(icallbacks.serverversion[server], 1)
473 cb({version = version, server = server, comments = comments})
474 if #icallbacks.serverversion[server] > 0 then send("VERSION", server)
475 else icallbacks.serverversion[server] = nil
481 function on_rpl_time(from, server, time)
482 local cb = table.remove(icallbacks.servertime[server], 1)
483 cb({time = time, server = server})
484 if #icallbacks.servertime[server] > 0 then send("TIME", server)
485 else icallbacks.servertime[server] = nil
494 function ctcp_handlers.on_action(from, to, message)
495 if to:sub(1, 1) == "#" then
496 base.assert(serverinfo.channels[to],
497 "Received channel msg from unknown channel: " .. to)
498 misc._try_call(on_channel_act, serverinfo.channels[to], from, message)
500 misc._try_call(on_private_act, from, message)
506 -- TODO: can we not have this handler be registered unless the dcc module is
508 function ctcp_handlers.on_dcc(from, to, message)
509 local type, argument, address, port, size = base.unpack(misc._split(message, " ", nil, '"', '"'))
510 if type == "SEND" then
511 if misc._try_call(on_dcc, from, to, argument, address, port, size) then
512 dcc._accept(argument, address, port)
514 elseif type == "CHAT" then
515 -- TODO: implement this? do people ever use this?
521 function ctcp_handlers.on_version(from, to)
522 notice(from, c("VERSION", _VERSION .. " running under " .. base._VERSION .. " with " .. socket._VERSION))
527 function ctcp_handlers.on_errmsg(from, to, message)
528 notice(from, c("ERRMSG", message .. "No error has occurred"))
533 function ctcp_handlers.on_ping(from, to, timestamp)
534 notice(from, c("PING", timestamp))
539 function ctcp_handlers.on_time(from, to)
540 notice(from, c("TIME", os.date()))
547 -- actions are handled the same, notice or not
548 ctcp_handlers.on_rpl_action = ctcp_handlers.on_action
551 -- on_rpl_version {{{
552 function ctcp_handlers.on_rpl_version(from, to, version)
553 local lfrom = from:lower()
554 local cb = table.remove(icallbacks.ctcp_version[lfrom], 1)
555 cb({version = version, nick = from})
556 if #icallbacks.ctcp_version[lfrom] > 0 then say(from, c("VERSION"))
557 else icallbacks.ctcp_version[lfrom] = nil
563 function ctcp_handlers.on_rpl_errmsg(from, to, message)
564 try_call(on_ctcp_error, from, to, message)
569 function ctcp_handlers.on_rpl_ping(from, to, timestamp)
570 local lfrom = from:lower()
571 local cb = table.remove(icallbacks.ctcp_ping[lfrom], 1)
572 cb({time = os.time() - timestamp, nick = from})
573 if #icallbacks.ctcp_ping[lfrom] > 0 then say(from, c("PING", os.time()))
574 else icallbacks.ctcp_ping[lfrom] = nil
580 function ctcp_handlers.on_rpl_time(from, to, time)
581 local lfrom = from:lower()
582 local cb = table.remove(icallbacks.ctcp_time[lfrom], 1)
583 cb({time = time, nick = from})
584 if #icallbacks.ctcp_time[lfrom] > 0 then say(from, c("TIME"))
585 else icallbacks.ctcp_time[lfrom] = nil
593 -- module functions {{{
594 -- socket handling functions {{{
595 -- _register_socket {{{
597 -- Register a socket to listen on.
598 -- @param sock LuaSocket socket object
599 -- @param mode 'r' if the socket is for reading, 'w' if for writing
600 -- @param cb Callback to call when the socket is ready for reading/writing.
601 -- It will be called with the socket as the single argument.
602 function _register_socket(sock, mode, cb)
611 base.assert(not cbs[sock], "socket already registered")
612 table.insert(socks, sock)
617 -- _unregister_socket {{{
619 -- Remove a previously registered socket.
620 -- @param sock Socket to unregister
621 -- @param mode 'r' to unregister it for reading, 'w' for writing
622 function _unregister_socket(sock, mode)
631 for i, v in base.ipairs(socks) do
632 if v == sock then table.remove(socks, i); break; end
640 -- public functions {{{
641 -- server commands {{{
644 -- Start a connection to the irc server.
645 -- @param args Table of named arguments containing connection parameters.
646 -- Defaults are the all-caps versions of these parameters given
647 -- at the top of the file, and are overridable by setting them
648 -- as well, i.e. <pre>irc.NETWORK = irc.freenode.net</pre>
649 -- Possible options are:
651 -- <li><i>network:</i> address of the irc network to connect to
652 -- (default: 'localhost')</li>
653 -- <li><i>port:</i> port to connect to
654 -- (default: '6667')</li>
655 -- <li><i>pass:</i> irc server password
656 -- (default: don't send)</li>
657 -- <li><i>nick:</i> nickname to connect as
658 -- (default: 'luabot')</li>
659 -- <li><i>username:</i> username to connect with
660 -- (default: 'LuaIRC')</li>
661 -- <li><i>realname:</i> realname to connect with
662 -- (default: 'LuaIRC')</li>
663 -- <li><i>timeout:</i> amount of time in seconds to wait before
664 -- dropping an idle connection
665 -- (default: '60')</li>
667 function connect(args)
668 local network = args.network or NETWORK
669 local port = args.port or PORT
670 local nick = args.nick or NICK
671 local username = args.username or USERNAME
672 local realname = args.realname or REALNAME
673 local timeout = args.timeout or TIMEOUT
674 serverinfo.connecting = true
675 if OUTFILE then irc_debug.set_output(OUTFILE) end
676 if DEBUG then irc_debug.enable() end
677 irc_sock = base.assert(socket.connect(network, port))
678 irc_sock:settimeout(timeout)
679 _register_socket(irc_sock, 'r', incoming_message)
680 if args.pass then send("PASS", args.pass) end
682 send("USER", username, get_ip(), network, realname)
689 -- Close the connection to the irc server.
690 -- @param message Quit message (optional, defaults to 'Leaving')
691 function quit(message)
692 message = message or "Leaving"
693 send("QUIT", message)
694 serverinfo.connected = false
701 -- @param channel Channel to join
702 function join(channel)
703 if not channel then return end
704 serverinfo.channels[channel] = Channel.new(channel)
705 send("JOIN", channel)
712 -- @param channel Channel to leave
713 function part(channel)
714 if not channel then return end
715 serverinfo.channels[channel] = nil
716 send("PART", channel)
722 -- Send a message to a user or channel.
723 -- @param name User or channel to send the message to
724 -- @param message Message to send
725 function say(name, message)
726 if not name then return end
727 message = message or ""
728 send("PRIVMSG", name, message)
734 -- Send a notice to a user or channel.
735 -- @param name User or channel to send the notice to
736 -- @param message Message to send
737 function notice(name, message)
738 if not name then return end
739 message = message or ""
740 send("NOTICE", name, message)
746 -- Perform a /me action.
747 -- @param name User or channel to send the action to
748 -- @param action Action to send
749 function act(name, action)
750 if not name then return end
751 action = action or ""
752 send("PRIVMSG", name, c("ACTION", action))
757 -- information requests {{{
758 -- server_version {{{
760 -- Request the version of the IRC server you are currently connected to.
761 -- @param cb Callback to call when the information is available. The single
762 -- table parameter to this callback will contain the fields:
764 -- <li><i>server:</i> the server which responded to the request</li>
765 -- <li><i>version:</i> the server version</li>
766 -- <li><i>comments:</i> other data provided by the server</li>
768 function server_version(cb)
769 -- apparently the optional server parameter isn't supported for servers
770 -- which you are not directly connected to (freenode specific?)
771 local server = serverinfo.host
772 if not icallbacks.serverversion[server] then
773 icallbacks.serverversion[server] = {cb}
774 send("VERSION", server)
776 table.insert(icallbacks.serverversion[server], cb)
782 -- TODO: allow server parameter (to get user idle time)
784 -- Request WHOIS information about a given user.
785 -- @param cb Callback to call when the information is available. The single
786 -- table parameter to this callback may contain any or all of the
789 -- <li><i>nick:</i> the nick that was passed to this function
790 -- (this field will always be here)</li>
791 -- <li><i>user:</i> the IRC username of the user</li>
792 -- <li><i>host:</i> the user's hostname</li>
793 -- <li><i>realname:</i> the IRC realname of the user</li>
794 -- <li><i>server:</i> the IRC server the user is connected to</li>
795 -- <li><i>serverinfo:</i> arbitrary information about the above
797 -- <li><i>awaymsg:</i> set to the user's away message if they are
799 -- <li><i>is_oper:</i> true if the user is an IRCop</li>
800 -- <li><i>idle_time:</i> amount of time the user has been idle</li>
801 -- <li><i>channels:</i> array containing the channels the user has
804 -- @param nick User to request WHOIS information about
805 function whois(cb, nick)
807 requestinfo.whois[nick] = {}
808 if not icallbacks.whois[nick] then
809 icallbacks.whois[nick] = {cb}
812 table.insert(icallbacks.whois[nick], cb)
819 -- Request the current time of the server you are connected to.
820 -- @param cb Callback to call when the information is available. The single
821 -- table parameter to this callback will contain the fields:
823 -- <li><i>server:</i> the server which responded to the request</li>
824 -- <li><i>time:</i> the time reported by the server</li>
826 function server_time(cb)
827 -- apparently the optional server parameter isn't supported for servers
828 -- which you are not directly connected to (freenode specific?)
829 local server = serverinfo.host
830 if not icallbacks.servertime[server] then
831 icallbacks.servertime[server] = {cb}
834 table.insert(icallbacks.servertime[server], cb)
843 -- Send a CTCP ping request.
844 -- @param cb Callback to call when the information is available. The single
845 -- table parameter to this callback will contain the fields:
847 -- <li><i>nick:</i> the nick which responded to the request</li>
848 -- <li><i>time:</i> the roundtrip ping time, in seconds</li>
850 -- @param nick User to ping
851 function ctcp_ping(cb, nick)
853 if not icallbacks.ctcp_ping[nick] then
854 icallbacks.ctcp_ping[nick] = {cb}
855 say(nick, c("PING", os.time()))
857 table.insert(icallbacks.ctcp_ping[nick], cb)
864 -- Send a localtime request.
865 -- @param cb Callback to call when the information is available. The single
866 -- table parameter to this callback will contain the fields:
868 -- <li><i>nick:</i> the nick which responded to the request</li>
869 -- <li><i>time:</i> the localtime reported by the remote client</li>
871 -- @param nick User to request the localtime from
872 function ctcp_time(cb, nick)
874 if not icallbacks.ctcp_time[nick] then
875 icallbacks.ctcp_time[nick] = {cb}
878 table.insert(icallbacks.ctcp_time[nick], cb)
885 -- Send a client version request.
886 -- @param cb Callback to call when the information is available. The single
887 -- table parameter to this callback will contain the fields:
889 -- <li><i>nick:</i> the nick which responded to the request</li>
890 -- <li><i>version:</i> the version reported by the remote client</li>
892 -- @param nick User to request the client version from
893 function ctcp_version(cb, nick)
895 if not icallbacks.ctcp_version[nick] then
896 icallbacks.ctcp_version[nick] = {cb}
897 say(nick, c("VERSION"))
899 table.insert(icallbacks.ctcp_version[nick], cb)
905 -- misc functions {{{
907 -- TODO: CTCP quoting should be explicit, this table thing is quite ugly (if
910 -- Send a raw IRC command.
911 -- @param command String containing the raw IRC command
912 -- @param ... Arguments to the command. Each argument is either a string or
913 -- an array. Strings are sent literally, arrays are CTCP quoted
914 -- as a group. The last argument (if it exists) is preceded by
915 -- a : (so it may contain spaces).
916 function send(command, ...)
917 if not serverinfo.connected and not serverinfo.connecting then return end
918 local message = command
919 for i, v in base.ipairs({...}) do
923 message = message .. " " .. v
925 message = ctcp._low_quote(message)
926 -- we just truncate for now. -2 to account for the \r\n
927 message = message:sub(1, constants.IRC_MAX_MSG - 2)
928 irc_debug._message("SEND", message)
929 irc_sock:send(message .. "\r\n")
935 -- Get the local IP address for the server connection.
936 -- @return A string representation of the local IP address that the IRC server
937 -- connection is communicating on
939 return (ip or irc_sock:getsockname())
945 -- Set the local IP manually (to allow for NAT workarounds)
946 -- @param new_ip IP address to set
947 function set_ip(new_ip)
953 -- TODO: @see doesn't currently work for files/modules
955 -- Iterate over currently joined channels.
956 -- channels() is an iterator function for use in for loops.
957 -- For example, <pre>for chan in irc.channels() do print(chan:name) end</pre>
960 return function(state, arg)
961 return misc._value_iter(state, arg,
963 return v.join_complete