2 -- Implementation of the main LuaIRC module
6 local constants = require 'irc.constants'
7 local irc_debug = require 'irc.debug'
8 local message = require 'irc.message'
9 local misc = require 'irc.misc'
10 local socket = require 'socket'
11 local os = require 'os'
12 local string = require 'string'
13 local table = require 'table'
17 -- LuaIRC - IRC framework written in Lua
22 _VERSION = 'LuaIRC 0.2'
26 local Channel = base.require 'irc.channel'
29 -- local variables {{{
43 local requestinfo = {whois = {}}
45 local ctcp_handlers = {}
51 TIMEOUT = 60 -- connection timeout
52 NETWORK = "localhost" -- default network
53 PORT = 6667 -- default port
54 NICK = "luabot" -- default nick
55 USERNAME = "LuaIRC" -- default username
56 REALNAME = "LuaIRC" -- default realname
57 DEBUG = false -- whether we want extra debug information
58 OUTFILE = nil -- file to send debug output to - nil is stdout
61 -- private functions {{{
63 local function main_loop_iter()
64 if #rsockets == 0 and #wsockets == 0 then return false end
65 local rready, wready, err = socket.select(rsockets, wsockets)
66 if err then irc_debug._err(err); return false; end
68 for _, sock in base.ipairs(rready) do
69 local cb = socket.protect(rcallbacks[sock])
70 local ret, err = cb(sock)
72 irc_debug._warn("socket error: " .. err)
73 _unregister_socket(sock, 'r')
77 for _, sock in base.ipairs(wready) do
78 local cb = socket.protect(wcallbacks[sock])
79 local ret, err = cb(sock)
81 irc_debug._warn("socket error: " .. err)
82 _unregister_socket(sock, 'w')
90 -- begin_main_loop {{{
91 local function begin_main_loop()
92 while main_loop_iter() do end
96 -- incoming_message {{{
97 local function incoming_message(sock)
98 local raw_msg = socket.try(sock:receive())
99 irc_debug._message("RECV", raw_msg)
100 local msg = message._parse(raw_msg)
101 misc._try_call_warn("Unhandled server message: " .. msg.command,
102 handlers["on_" .. msg.command:lower()],
103 (misc.parse_user(msg.from)), base.unpack(msg.args))
109 -- internal message handlers {{{
110 -- command handlers {{{
112 function handlers.on_nick(from, new_nick)
113 for chan in channels() do
114 chan:_change_nick(from, new_nick)
116 misc._try_call(on_nick_change, new_nick, from)
121 function handlers.on_join(from, chan)
122 base.assert(serverinfo.channels[chan],
123 "Received join message for unknown channel: " .. chan)
124 if serverinfo.channels[chan].join_complete then
125 serverinfo.channels[chan]:_add_user(from)
126 misc._try_call(on_join, serverinfo.channels[chan], from)
132 function handlers.on_part(from, chan, part_msg)
133 -- don't assert on chan here, since we get part messages for ourselves
134 -- after we remove the channel from the channel list
135 if not serverinfo.channels[chan] then return end
136 if serverinfo.channels[chan].join_complete then
137 serverinfo.channels[chan]:_remove_user(from)
138 misc._try_call(on_part, serverinfo.channels[chan], from, part_msg)
144 function handlers.on_mode(from, to, mode_string, ...)
145 local dir = mode_string:sub(1, 1)
146 mode_string = mode_string:sub(2)
149 if to:sub(1, 1) == "#" then
150 -- handle channel mode requests {{{
151 base.assert(serverinfo.channels[to],
152 "Received mode change for unknown channel: " .. to)
153 local chan = serverinfo.channels[to]
155 for i = 1, mode_string:len() do
156 local mode = mode_string:sub(i, i)
157 local target = args[ind]
158 -- channel modes other than op/voice will be implemented as
159 -- information request commands
160 if mode == "o" then -- channel op {{{
161 chan:_change_status(target, dir == "+", "o")
162 misc._try_call(({["+"] = on_op, ["-"] = on_deop})[dir],
166 elseif mode == "v" then -- voice {{{
167 chan:_change_status(target, dir == "+", "v")
168 misc._try_call(({["+"] = on_voice, ["-"] = on_devoice})[dir],
175 elseif from == to then
176 -- handle user mode requests {{{
177 -- TODO: make users more easily accessible so this is actually
178 -- reasonably possible
179 for i = 1, mode_string:len() do
180 local mode = mode_string:sub(i, i)
181 if mode == "i" then -- invisible {{{
183 elseif mode == "s" then -- server messages {{{
185 elseif mode == "w" then -- wallops messages {{{
187 elseif mode == "o" then -- ircop {{{
197 function handlers.on_topic(from, chan, new_topic)
198 base.assert(serverinfo.channels[chan],
199 "Received topic message for unknown channel: " .. chan)
200 serverinfo.channels[chan]._topic.text = new_topic
201 serverinfo.channels[chan]._topic.user = (misc.parse_user(from))
202 serverinfo.channels[chan]._topic.time = os.time()
203 if serverinfo.channels[chan].join_complete then
204 misc._try_call(on_topic_change, serverinfo.channels[chan])
210 function handlers.on_invite(from, to, chan)
211 misc._try_call(on_invite, from, chan)
216 function handlers.on_kick(from, chan, to)
217 base.assert(serverinfo.channels[chan],
218 "Received kick message for unknown channel: " .. chan)
219 if serverinfo.channels[chan].join_complete then
220 serverinfo.channels[chan]:_remove_user(to)
221 misc._try_call(on_kick, serverinfo.channels[chan], to, from)
227 function handlers.on_privmsg(from, to, msg)
228 local msgs = ctcp._ctcp_split(msg, true)
229 for _, v in base.ipairs(msgs) do
230 if base.type(v) == "string" then
231 -- normal message {{{
232 if to:sub(1, 1) == "#" then
233 base.assert(serverinfo.channels[to],
234 "Received channel msg from unknown channel: " .. to)
235 misc._try_call(on_channel_msg, serverinfo.channels[to], from, v)
237 misc._try_call(on_private_msg, from, v)
240 elseif base.type(v) == "table" then
242 local words = misc._split(v[1])
243 local received_command = words[1]
244 local cb = "on_" .. received_command:lower()
245 table.remove(words, 1)
246 -- not using try_call here because the ctcp specification requires
247 -- an error response to nonexistant commands
248 if base.type(ctcp_handlers[cb]) == "function" then
249 ctcp_handlers[cb](from, to, table.concat(words, " "))
251 notice(from, {"ERRMSG Unknown query: " .. received_command})
260 function handlers.on_notice(from, to, msg)
261 local msgs = ctcp._ctcp_split(msg, true)
262 for _, v in base.ipairs(msgs) do
263 if base.type(v) == "string" then
264 -- normal message {{{
265 if to:sub(1, 1) == "#" then
266 base.assert(serverinfo.channels[to],
267 "Received channel msg from unknown channel: " .. to)
268 misc._try_call(on_channel_notice, serverinfo.channels[to],
271 misc._try_call(on_private_notice, from, v)
274 elseif base.type(v) == "table" then
276 local words = misc._split(v[1])
277 local command = words[1]:lower()
278 table.remove(words, 1)
279 misc._try_call_warn("Unknown CTCP message: " .. command,
280 ctcp_handlers["on_rpl_"..command], from, to,
281 table.concat(words, ' '))
289 function handlers.on_quit(from, quit_msg)
290 for name, chan in base.pairs(serverinfo.channels) do
291 chan:_remove_user(from)
293 misc._try_call(on_quit, from, quit_msg)
298 -- respond to server pings to make sure it knows we are alive
299 function handlers.on_ping(from, respond_to)
300 send("PONG", respond_to)
305 -- server replies {{{
307 -- catch topic changes
308 function handlers.on_rpl_topic(from, chan, topic)
309 base.assert(serverinfo.channels[chan],
310 "Received topic information about unknown channel: " .. chan)
311 serverinfo.channels[chan]._topic.text = topic
315 -- on_rpl_notopic {{{
316 function handlers.on_rpl_notopic(from, chan)
317 base.assert(serverinfo.channels[chan],
318 "Received topic information about unknown channel: " .. chan)
319 serverinfo.channels[chan]._topic.text = ""
323 -- on_rpl_topicdate {{{
324 -- "topic was set by <user> at <time>"
325 function handlers.on_rpl_topicdate(from, chan, user, time)
326 base.assert(serverinfo.channels[chan],
327 "Received topic information about unknown channel: " .. chan)
328 serverinfo.channels[chan]._topic.user = user
329 serverinfo.channels[chan]._topic.time = base.tonumber(time)
333 -- on_rpl_namreply {{{
334 -- handles a NAMES reply
335 function handlers.on_rpl_namreply(from, chanmode, chan, userlist)
336 base.assert(serverinfo.channels[chan],
337 "Received user information about unknown channel: " .. chan)
338 serverinfo.channels[chan]._chanmode = constants.chanmodes[chanmode]
339 local users = misc._split(userlist)
340 for k,v in base.ipairs(users) do
341 if v:sub(1, 1) == "@" or v:sub(1, 1) == "+" then
342 local nick = v:sub(2)
343 serverinfo.channels[chan]:_add_user(nick, v:sub(1, 1))
345 serverinfo.channels[chan]:_add_user(v)
351 -- on_rpl_endofnames {{{
352 -- when we get this message, the channel join has completed, so call the
354 function handlers.on_rpl_endofnames(from, chan)
355 base.assert(serverinfo.channels[chan],
356 "Received user information about unknown channel: " .. chan)
357 if not serverinfo.channels[chan].join_complete then
358 misc._try_call(on_me_join, serverinfo.channels[chan])
359 serverinfo.channels[chan].join_complete = true
364 -- on_rpl_welcome {{{
365 function handlers.on_rpl_welcome(from)
374 -- on_rpl_yourhost {{{
375 function handlers.on_rpl_yourhost(from, msg)
376 serverinfo.host = from
380 -- on_rpl_motdstart {{{
381 function handlers.on_rpl_motdstart(from)
387 function handlers.on_rpl_motd(from, motd)
388 serverinfo.motd = (serverinfo.motd or "") .. motd .. "\n"
392 -- on_rpl_endofmotd {{{
393 function handlers.on_rpl_endofmotd(from)
394 if not serverinfo.connected then
395 serverinfo.connected = true
396 serverinfo.connecting = false
397 misc._try_call(on_connect)
402 -- on_rpl_whoisuser {{{
403 function handlers.on_rpl_whoisuser(from, nick, user, host, star, realname)
404 local lnick = nick:lower()
405 requestinfo.whois[lnick].nick = nick
406 requestinfo.whois[lnick].user = user
407 requestinfo.whois[lnick].host = host
408 requestinfo.whois[lnick].realname = realname
412 -- on_rpl_whoischannels {{{
413 function handlers.on_rpl_whoischannels(from, nick, channel_list)
415 if not requestinfo.whois[nick].channels then
416 requestinfo.whois[nick].channels = {}
418 for _, channel in base.ipairs(misc._split(channel_list)) do
419 table.insert(requestinfo.whois[nick].channels, channel)
424 -- on_rpl_whoisserver {{{
425 function handlers.on_rpl_whoisserver(from, nick, server, serverinfo)
427 requestinfo.whois[nick].server = server
428 requestinfo.whois[nick].serverinfo = serverinfo
433 function handlers.on_rpl_away(from, nick, away_msg)
435 if requestinfo.whois[nick] then
436 requestinfo.whois[nick].away_msg = away_msg
441 -- on_rpl_whoisoperator {{{
442 function handlers.on_rpl_whoisoperator(from, nick)
443 requestinfo.whois[nick:lower()].is_oper = true
447 -- on_rpl_whoisidle {{{
448 function handlers.on_rpl_whoisidle(from, nick, idle_seconds)
449 requestinfo.whois[nick:lower()].idle_time = idle_seconds
453 -- on_rpl_endofwhois {{{
454 function handlers.on_rpl_endofwhois(from, nick)
456 local cb = table.remove(icallbacks.whois[nick], 1)
457 cb(requestinfo.whois[nick])
458 requestinfo.whois[nick] = nil
459 if #icallbacks.whois[nick] > 0 then send("WHOIS", nick)
460 else icallbacks.whois[nick] = nil
465 -- on_rpl_version {{{
466 function handlers.on_rpl_version(from, version, server, comments)
467 local cb = table.remove(icallbacks.serverversion[server], 1)
468 cb({version = version, server = server, comments = comments})
469 if #icallbacks.serverversion[server] > 0 then send("VERSION", server)
470 else icallbacks.serverversion[server] = nil
476 function on_rpl_time(from, server, time)
477 local cb = table.remove(icallbacks.servertime[server], 1)
478 cb({time = time, server = server})
479 if #icallbacks.servertime[server] > 0 then send("TIME", server)
480 else icallbacks.servertime[server] = nil
489 function ctcp_handlers.on_action(from, to, message)
490 if to:sub(1, 1) == "#" then
491 base.assert(serverinfo.channels[to],
492 "Received channel msg from unknown channel: " .. to)
493 misc._try_call(on_channel_act, serverinfo.channels[to], from, message)
495 misc._try_call(on_private_act, from, message)
501 -- TODO: can we not have this handler be registered unless the dcc module is
503 function ctcp_handlers.on_dcc(from, to, message)
504 local type, argument, address, port, size = base.unpack(misc._split(message, " ", nil, '"', '"'))
505 if type == "SEND" then
506 if misc._try_call(on_dcc, from, to, argument, address, port, size) then
507 dcc._accept(argument, address, port)
509 elseif type == "CHAT" then
510 -- TODO: implement this? do people ever use this?
516 function ctcp_handlers.on_version(from, to)
517 notice(from, {"VERSION " .. _VERSION .. " running under " .. base._VERSION .. " with " .. socket._VERSION})
522 function ctcp_handlers.on_errmsg(from, to, message)
523 notice(from, {"ERRMSG " .. message .. "No error has occurred"})
528 function ctcp_handlers.on_ping(from, to, timestamp)
529 notice(from, {"PING " .. timestamp})
534 function ctcp_handlers.on_time(from, to)
535 notice(from, {"TIME " .. os.date()})
542 -- actions are handled the same, notice or not
543 ctcp_handlers.on_rpl_action = ctcp_handlers.on_action
546 -- on_rpl_version {{{
547 function ctcp_handlers.on_rpl_version(from, to, version)
548 local lfrom = from:lower()
549 local cb = table.remove(icallbacks.ctcp_version[lfrom], 1)
550 cb({version = version, nick = from})
551 if #icallbacks.ctcp_version[lfrom] > 0 then say(from, {"VERSION"})
552 else icallbacks.ctcp_version[lfrom] = nil
558 function ctcp_handlers.on_rpl_errmsg(from, to, message)
559 try_call(on_ctcp_error, from, to, message)
564 function ctcp_handlers.on_rpl_ping(from, to, timestamp)
565 local lfrom = from:lower()
566 local cb = table.remove(icallbacks.ctcp_ping[lfrom], 1)
567 cb({time = os.time() - timestamp, nick = from})
568 if #icallbacks.ctcp_ping[lfrom] > 0 then say(from, {"PING " .. os.time()})
569 else icallbacks.ctcp_ping[lfrom] = nil
575 function ctcp_handlers.on_rpl_time(from, to, time)
576 local lfrom = from:lower()
577 local cb = table.remove(icallbacks.ctcp_time[lfrom], 1)
578 cb({time = time, nick = from})
579 if #icallbacks.ctcp_time[lfrom] > 0 then say(from, {"TIME"})
580 else icallbacks.ctcp_time[lfrom] = nil
588 -- module functions {{{
589 -- socket handling functions {{{
590 -- _register_socket {{{
592 -- Register a socket to listen on.
593 -- @param sock LuaSocket socket object
594 -- @param mode 'r' if the socket is for reading, 'w' if for writing
595 -- @param cb Callback to call when the socket is ready for reading/writing.
596 -- It will be called with the socket as the single argument.
597 function _register_socket(sock, mode, cb)
606 base.assert(not cbs[sock], "socket already registered")
607 table.insert(socks, sock)
612 -- _unregister_socket {{{
614 -- Remove a previously registered socket.
615 -- @param sock Socket to unregister
616 -- @param mode 'r' to unregister it for reading, 'w' for writing
617 function _unregister_socket(sock, mode)
626 for i, v in base.ipairs(socks) do
627 if v == sock then table.remove(socks, i); break; end
635 -- public functions {{{
636 -- server commands {{{
639 -- Start a connection to the irc server.
640 -- @param args Table of named arguments containing connection parameters.
641 -- Defaults are the all-caps versions of these parameters given
642 -- at the top of the file, and are overridable by setting them
643 -- as well, i.e. <pre>irc.NETWORK = irc.freenode.net</pre>
644 -- Possible options are:
646 -- <li><i>network:</i> address of the irc network to connect to
647 -- (default: 'localhost')</li>
648 -- <li><i>port:</i> port to connect to
649 -- (default: '6667')</li>
650 -- <li><i>pass:</i> irc server password
651 -- (default: don't send)</li>
652 -- <li><i>nick:</i> nickname to connect as
653 -- (default: 'luabot')</li>
654 -- <li><i>username:</i> username to connect with
655 -- (default: 'LuaIRC')</li>
656 -- <li><i>realname:</i> realname to connect with
657 -- (default: 'LuaIRC')</li>
658 -- <li><i>timeout:</i> amount of time in seconds to wait before
659 -- dropping an idle connection
660 -- (default: '60')</li>
662 function connect(args)
663 local network = args.network or NETWORK
664 local port = args.port or PORT
665 local nick = args.nick or NICK
666 local username = args.username or USERNAME
667 local realname = args.realname or REALNAME
668 local timeout = args.timeout or TIMEOUT
669 serverinfo.connecting = true
670 if OUTFILE then irc_debug.set_output(OUTFILE) end
671 if DEBUG then irc_debug.enable() end
672 irc_sock = base.assert(socket.connect(network, port))
673 irc_sock:settimeout(timeout)
674 _register_socket(irc_sock, 'r', incoming_message)
675 if args.pass then send("PASS", args.pass) end
677 send("USER", username, get_ip(), network, realname)
684 -- Close the connection to the irc server.
685 -- @param message Quit message (optional, defaults to 'Leaving')
686 function quit(message)
687 message = message or "Leaving"
688 send("QUIT", message)
689 serverinfo.connected = false
696 -- @param channel Channel to join
697 function join(channel)
698 if not channel then return end
699 serverinfo.channels[channel] = Channel.new(channel)
700 send("JOIN", channel)
707 -- @param channel Channel to leave
708 function part(channel)
709 if not channel then return end
710 serverinfo.channels[channel] = nil
711 send("PART", channel)
717 -- Send a message to a user or channel.
718 -- @param name User or channel to send the message to
719 -- @param message Message to send
720 function say(name, message)
721 if not name then return end
722 message = message or ""
723 send("PRIVMSG", name, message)
729 -- Send a notice to a user or channel.
730 -- @param name User or channel to send the notice to
731 -- @param message Message to send
732 function notice(name, message)
733 if not name then return end
734 message = message or ""
735 send("NOTICE", name, message)
741 -- Perform a /me action.
742 -- @param name User or channel to send the action to
743 -- @param action Action to send
744 function act(name, action)
745 if not name then return end
746 action = action or ""
747 send("PRIVMSG", name, {"ACTION", action})
752 -- information requests {{{
753 -- server_version {{{
755 -- Request the version of the IRC server you are currently connected to.
756 -- @param cb Callback to call when the information is available. The single
757 -- table parameter to this callback will contain the fields:
759 -- <li><i>server:</i> the server which responded to the request</li>
760 -- <li><i>version:</i> the server version</li>
761 -- <li><i>comments:</i> other data provided by the server</li>
763 function server_version(cb)
764 -- apparently the optional server parameter isn't supported for servers
765 -- which you are not directly connected to (freenode specific?)
766 local server = serverinfo.host
767 if not icallbacks.serverversion[server] then
768 icallbacks.serverversion[server] = {cb}
769 send("VERSION", server)
771 table.insert(icallbacks.serverversion[server], cb)
777 -- TODO: allow server parameter (to get user idle time)
779 -- Request WHOIS information about a given user.
780 -- @param cb Callback to call when the information is available. The single
781 -- table parameter to this callback may contain any or all of the
784 -- <li><i>nick:</i> the nick that was passed to this function
785 -- (this field will always be here)</li>
786 -- <li><i>user:</i> the IRC username of the user</li>
787 -- <li><i>host:</i> the user's hostname</li>
788 -- <li><i>realname:</i> the IRC realname of the user</li>
789 -- <li><i>server:</i> the IRC server the user is connected to</li>
790 -- <li><i>serverinfo:</i> arbitrary information about the above
792 -- <li><i>awaymsg:</i> set to the user's away message if they are
794 -- <li><i>is_oper:</i> true if the user is an IRCop</li>
795 -- <li><i>idle_time:</i> amount of time the user has been idle</li>
796 -- <li><i>channels:</i> array containing the channels the user has
799 -- @param nick User to request WHOIS information about
800 function whois(cb, nick)
802 requestinfo.whois[nick] = {}
803 if not icallbacks.whois[nick] then
804 icallbacks.whois[nick] = {cb}
807 table.insert(icallbacks.whois[nick], cb)
814 -- Request the current time of the server you are connected to.
815 -- @param cb Callback to call when the information is available. The single
816 -- table parameter to this callback will contain the fields:
818 -- <li><i>server:</i> the server which responded to the request</li>
819 -- <li><i>time:</i> the time reported by the server</li>
821 function server_time(cb)
822 -- apparently the optional server parameter isn't supported for servers
823 -- which you are not directly connected to (freenode specific?)
824 local server = serverinfo.host
825 if not icallbacks.servertime[server] then
826 icallbacks.servertime[server] = {cb}
829 table.insert(icallbacks.servertime[server], cb)
838 -- Send a CTCP ping request.
839 -- @param cb Callback to call when the information is available. The single
840 -- table parameter to this callback will contain the fields:
842 -- <li><i>nick:</i> the nick which responded to the request</li>
843 -- <li><i>time:</i> the roundtrip ping time, in seconds</li>
845 -- @param nick User to ping
846 function ctcp_ping(cb, nick)
848 if not icallbacks.ctcp_ping[nick] then
849 icallbacks.ctcp_ping[nick] = {cb}
850 say(nick, {"PING " .. os.time()})
852 table.insert(icallbacks.ctcp_ping[nick], cb)
859 -- Send a localtime request.
860 -- @param cb Callback to call when the information is available. The single
861 -- table parameter to this callback will contain the fields:
863 -- <li><i>nick:</i> the nick which responded to the request</li>
864 -- <li><i>time:</i> the localtime reported by the remote client</li>
866 -- @param nick User to request the localtime from
867 function ctcp_time(cb, nick)
869 if not icallbacks.ctcp_time[nick] then
870 icallbacks.ctcp_time[nick] = {cb}
873 table.insert(icallbacks.ctcp_time[nick], cb)
880 -- Send a client version request.
881 -- @param cb Callback to call when the information is available. The single
882 -- table parameter to this callback will contain the fields:
884 -- <li><i>nick:</i> the nick which responded to the request</li>
885 -- <li><i>version:</i> the version reported by the remote client</li>
887 -- @param nick User to request the client version from
888 function ctcp_version(cb, nick)
890 if not icallbacks.ctcp_version[nick] then
891 icallbacks.ctcp_version[nick] = {cb}
892 say(nick, {"VERSION"})
894 table.insert(icallbacks.ctcp_version[nick], cb)
900 -- misc functions {{{
902 -- TODO: CTCP quoting should be explicit, this table thing is quite ugly (if
905 -- Send a raw IRC command.
906 -- @param command String containing the raw IRC command
907 -- @param ... Arguments to the command. Each argument is either a string or
908 -- an array. Strings are sent literally, arrays are CTCP quoted
909 -- as a group. The last argument (if it exists) is preceded by
910 -- a : (so it may contain spaces).
911 function send(command, ...)
912 if not serverinfo.connected and not serverinfo.connecting then return end
913 local message = command
914 for i, v in base.ipairs({...}) do
916 -- passing a table in as an argument means to treat that table as a
917 -- CTCP command, so quote it appropriately
918 if base.type(v) == "string" then
920 elseif base.type(v) == "table" then
921 arg = ctcp._ctcp_quote(table.concat(v, " "))
926 message = message .. " " .. arg
928 message = ctcp._low_quote(message)
929 -- we just truncate for now. -2 to account for the \r\n
930 message = message:sub(1, constants.IRC_MAX_MSG - 2)
931 irc_debug._message("SEND", message)
932 irc_sock:send(message .. "\r\n")
938 -- Get the local IP address for the server connection.
939 -- @return A string representation of the local IP address that the IRC server
940 -- connection is communicating on
942 return (ip or irc_sock:getsockname())
948 -- Set the local IP manually (to allow for NAT workarounds)
949 -- @param new_ip IP address to set
950 function set_ip(new_ip)
956 -- TODO: @see doesn't currently work for files/modules
958 -- Iterate over currently joined channels.
959 -- channels() is an iterator function for use in for loops.
960 -- For example, <pre>for chan in irc.channels() do print(chan:name) end</pre>
963 return function(state, arg)
964 return misc._value_iter(state, arg,
966 return v.join_complete