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 = {}
48 local user_handlers = {}
54 TIMEOUT = 60 -- connection timeout
55 NETWORK = "localhost" -- default network
56 PORT = 6667 -- default port
57 NICK = "luabot" -- default nick
58 USERNAME = "LuaIRC" -- default username
59 REALNAME = "LuaIRC" -- default realname
60 DEBUG = false -- whether we want extra debug information
61 OUTFILE = nil -- file to send debug output to - nil is stdout
64 -- private functions {{{
66 local function main_loop_iter()
67 if #rsockets == 0 and #wsockets == 0 then return false end
68 local rready, wready, err = socket.select(rsockets, wsockets)
69 if err then irc_debug._err(err); return false; end
71 for _, sock in base.ipairs(rready) do
72 local cb = socket.protect(rcallbacks[sock])
73 local ret, err = cb(sock)
75 irc_debug._warn("socket error: " .. err)
76 _unregister_socket(sock, 'r')
80 for _, sock in base.ipairs(wready) do
81 local cb = socket.protect(wcallbacks[sock])
82 local ret, err = cb(sock)
84 irc_debug._warn("socket error: " .. err)
85 _unregister_socket(sock, 'w')
93 -- begin_main_loop {{{
94 local function begin_main_loop()
95 while main_loop_iter() do end
99 -- incoming_message {{{
100 local function incoming_message(sock)
101 local raw_msg = socket.try(sock:receive())
102 irc_debug._message("RECV", raw_msg)
103 local msg = message._parse(raw_msg)
104 misc._try_call_warn("Unhandled server message: " .. msg.command,
105 handlers["on_" .. msg.command:lower()],
106 (misc._parse_user(msg.from)), base.unpack(msg.args))
112 local function callback(name, ...)
113 return misc._try_call(user_handlers[name], ...)
118 -- internal message handlers {{{
119 -- command handlers {{{
121 function handlers.on_nick(from, new_nick)
122 for chan in channels() do
123 chan:_change_nick(from, new_nick)
125 callback("nick_change", new_nick, from)
130 function handlers.on_join(from, chan)
131 base.assert(serverinfo.channels[chan],
132 "Received join message for unknown channel: " .. chan)
133 if serverinfo.channels[chan].join_complete then
134 serverinfo.channels[chan]:_add_user(from)
135 callback("join", serverinfo.channels[chan], from)
141 function handlers.on_part(from, chan, part_msg)
142 -- don't assert on chan here, since we get part messages for ourselves
143 -- after we remove the channel from the channel list
144 if not serverinfo.channels[chan] then return end
145 if serverinfo.channels[chan].join_complete then
146 serverinfo.channels[chan]:_remove_user(from)
147 callback("part", serverinfo.channels[chan], from, part_msg)
153 function handlers.on_mode(from, to, mode_string, ...)
154 local dir = mode_string:sub(1, 1)
155 mode_string = mode_string:sub(2)
158 if to:sub(1, 1) == "#" then
159 -- handle channel mode requests {{{
160 base.assert(serverinfo.channels[to],
161 "Received mode change for unknown channel: " .. to)
162 local chan = serverinfo.channels[to]
164 for i = 1, mode_string:len() do
165 local mode = mode_string:sub(i, i)
166 local target = args[ind]
167 -- channel modes other than op/voice will be implemented as
168 -- information request commands
169 if mode == "o" then -- channel op {{{
170 chan:_change_status(target, dir == "+", "o")
171 callback(({["+"] = "op", ["-"] = "deop"})[dir],
175 elseif mode == "v" then -- voice {{{
176 chan:_change_status(target, dir == "+", "v")
177 callback(({["+"] = "voice", ["-"] = "devoice"})[dir],
184 elseif from == to then
185 -- handle user mode requests {{{
186 -- TODO: make users more easily accessible so this is actually
187 -- reasonably possible
188 for i = 1, mode_string:len() do
189 local mode = mode_string:sub(i, i)
190 if mode == "i" then -- invisible {{{
192 elseif mode == "s" then -- server messages {{{
194 elseif mode == "w" then -- wallops messages {{{
196 elseif mode == "o" then -- ircop {{{
206 function handlers.on_topic(from, chan, new_topic)
207 base.assert(serverinfo.channels[chan],
208 "Received topic message for unknown channel: " .. chan)
209 serverinfo.channels[chan]._topic.text = new_topic
210 serverinfo.channels[chan]._topic.user = from
211 serverinfo.channels[chan]._topic.time = os.time()
212 if serverinfo.channels[chan].join_complete then
213 callback("topic_change", serverinfo.channels[chan])
219 function handlers.on_invite(from, to, chan)
220 callback("invite", from, chan)
225 function handlers.on_kick(from, chan, to)
226 base.assert(serverinfo.channels[chan],
227 "Received kick message for unknown channel: " .. chan)
228 if serverinfo.channels[chan].join_complete then
229 serverinfo.channels[chan]:_remove_user(to)
230 callback("kick", serverinfo.channels[chan], to, from)
236 function handlers.on_privmsg(from, to, msg)
237 local msgs = ctcp._ctcp_split(msg)
238 for _, v in base.ipairs(msgs) do
242 local words = misc._split(msg)
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, c("ERRMSG", received_command, ":Unknown query"))
255 -- normal message {{{
256 if to:sub(1, 1) == "#" then
257 base.assert(serverinfo.channels[to],
258 "Received channel msg from unknown channel: " .. to)
259 callback("channel_msg", serverinfo.channels[to], from, msg)
261 callback("private_msg", from, msg)
270 function handlers.on_notice(from, to, msg)
271 local msgs = ctcp._ctcp_split(msg)
272 for _, v in base.ipairs(msgs) do
276 local words = misc._split(msg)
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, ' '))
284 -- normal message {{{
285 if to:sub(1, 1) == "#" then
286 base.assert(serverinfo.channels[to],
287 "Received channel msg from unknown channel: " .. to)
288 callback("channel_notice", serverinfo.channels[to], from, msg)
290 callback("private_notice", from, msg)
299 function handlers.on_quit(from, quit_msg)
300 for name, chan in base.pairs(serverinfo.channels) do
301 chan:_remove_user(from)
303 callback("quit", from, quit_msg)
308 -- respond to server pings to make sure it knows we are alive
309 function handlers.on_ping(from, respond_to)
310 send("PONG", respond_to)
315 -- server replies {{{
317 -- catch topic changes
318 function handlers.on_rpl_topic(from, chan, topic)
319 base.assert(serverinfo.channels[chan],
320 "Received topic information about unknown channel: " .. chan)
321 serverinfo.channels[chan]._topic.text = topic
325 -- on_rpl_notopic {{{
326 function handlers.on_rpl_notopic(from, chan)
327 base.assert(serverinfo.channels[chan],
328 "Received topic information about unknown channel: " .. chan)
329 serverinfo.channels[chan]._topic.text = ""
333 -- on_rpl_topicdate {{{
334 -- "topic was set by <user> at <time>"
335 function handlers.on_rpl_topicdate(from, chan, user, time)
336 base.assert(serverinfo.channels[chan],
337 "Received topic information about unknown channel: " .. chan)
338 serverinfo.channels[chan]._topic.user = user
339 serverinfo.channels[chan]._topic.time = base.tonumber(time)
343 -- on_rpl_namreply {{{
344 -- handles a NAMES reply
345 function handlers.on_rpl_namreply(from, chanmode, chan, userlist)
346 base.assert(serverinfo.channels[chan],
347 "Received user information about unknown channel: " .. chan)
348 serverinfo.channels[chan]._chanmode = constants.chanmodes[chanmode]
349 local users = misc._split(userlist)
350 for k,v in base.ipairs(users) do
351 if v:sub(1, 1) == "@" or v:sub(1, 1) == "+" then
352 local nick = v:sub(2)
353 serverinfo.channels[chan]:_add_user(nick, v:sub(1, 1))
355 serverinfo.channels[chan]:_add_user(v)
361 -- on_rpl_endofnames {{{
362 -- when we get this message, the channel join has completed, so call the
364 function handlers.on_rpl_endofnames(from, chan)
365 base.assert(serverinfo.channels[chan],
366 "Received user information about unknown channel: " .. chan)
367 if not serverinfo.channels[chan].join_complete then
368 callback("me_join", serverinfo.channels[chan])
369 serverinfo.channels[chan].join_complete = true
374 -- on_rpl_welcome {{{
375 function handlers.on_rpl_welcome(from)
384 -- on_rpl_yourhost {{{
385 function handlers.on_rpl_yourhost(from, msg)
386 serverinfo.host = from
390 -- on_rpl_motdstart {{{
391 function handlers.on_rpl_motdstart(from)
397 function handlers.on_rpl_motd(from, motd)
398 serverinfo.motd = (serverinfo.motd or "") .. motd .. "\n"
402 -- on_rpl_endofmotd {{{
403 function handlers.on_rpl_endofmotd(from)
404 if not serverinfo.connected then
405 serverinfo.connected = true
406 serverinfo.connecting = false
412 -- on_rpl_whoisuser {{{
413 function handlers.on_rpl_whoisuser(from, nick, user, host, star, realname)
414 local lnick = nick:lower()
415 requestinfo.whois[lnick].nick = nick
416 requestinfo.whois[lnick].user = user
417 requestinfo.whois[lnick].host = host
418 requestinfo.whois[lnick].realname = realname
422 -- on_rpl_whoischannels {{{
423 function handlers.on_rpl_whoischannels(from, nick, channel_list)
425 if not requestinfo.whois[nick].channels then
426 requestinfo.whois[nick].channels = {}
428 for _, channel in base.ipairs(misc._split(channel_list)) do
429 table.insert(requestinfo.whois[nick].channels, channel)
434 -- on_rpl_whoisserver {{{
435 function handlers.on_rpl_whoisserver(from, nick, server, serverinfo)
437 requestinfo.whois[nick].server = server
438 requestinfo.whois[nick].serverinfo = serverinfo
443 function handlers.on_rpl_away(from, nick, away_msg)
445 if requestinfo.whois[nick] then
446 requestinfo.whois[nick].away_msg = away_msg
451 -- on_rpl_whoisoperator {{{
452 function handlers.on_rpl_whoisoperator(from, nick)
453 requestinfo.whois[nick:lower()].is_oper = true
457 -- on_rpl_whoisidle {{{
458 function handlers.on_rpl_whoisidle(from, nick, idle_seconds)
459 requestinfo.whois[nick:lower()].idle_time = idle_seconds
463 -- on_rpl_endofwhois {{{
464 function handlers.on_rpl_endofwhois(from, nick)
466 local cb = table.remove(icallbacks.whois[nick], 1)
467 cb(requestinfo.whois[nick])
468 requestinfo.whois[nick] = nil
469 if #icallbacks.whois[nick] > 0 then send("WHOIS", nick)
470 else icallbacks.whois[nick] = nil
475 -- on_rpl_version {{{
476 function handlers.on_rpl_version(from, version, server, comments)
477 local cb = table.remove(icallbacks.serverversion[server], 1)
478 cb({version = version, server = server, comments = comments})
479 if #icallbacks.serverversion[server] > 0 then send("VERSION", server)
480 else icallbacks.serverversion[server] = nil
486 function on_rpl_time(from, server, time)
487 local cb = table.remove(icallbacks.servertime[server], 1)
488 cb({time = time, server = server})
489 if #icallbacks.servertime[server] > 0 then send("TIME", server)
490 else icallbacks.servertime[server] = nil
499 function ctcp_handlers.on_action(from, to, message)
500 if to:sub(1, 1) == "#" then
501 base.assert(serverinfo.channels[to],
502 "Received channel msg from unknown channel: " .. to)
503 callback("channel_act", serverinfo.channels[to], from, message)
505 callback("private_act", from, message)
511 -- TODO: can we not have this handler be registered unless the dcc module is
513 function ctcp_handlers.on_dcc(from, to, message)
514 local type, argument, address, port, size = base.unpack(misc._split(message, " ", nil, '"', '"'))
515 address = misc._ip_int_to_str(address)
516 if type == "SEND" then
517 if callback("dcc_send", from, to, argument, address, port, size) then
518 dcc._accept(argument, address, port)
520 elseif type == "CHAT" then
521 -- TODO: implement this? do people ever use this?
527 function ctcp_handlers.on_version(from, to)
528 notice(from, c("VERSION", _VERSION .. " running under " .. base._VERSION .. " with " .. socket._VERSION))
533 function ctcp_handlers.on_errmsg(from, to, message)
534 notice(from, c("ERRMSG", message, ":No error has occurred"))
539 function ctcp_handlers.on_ping(from, to, timestamp)
540 notice(from, c("PING", timestamp))
545 function ctcp_handlers.on_time(from, to)
546 notice(from, c("TIME", os.date()))
553 -- actions are handled the same, notice or not
554 ctcp_handlers.on_rpl_action = ctcp_handlers.on_action
557 -- on_rpl_version {{{
558 function ctcp_handlers.on_rpl_version(from, to, version)
559 local lfrom = from:lower()
560 local cb = table.remove(icallbacks.ctcp_version[lfrom], 1)
561 cb({version = version, nick = from})
562 if #icallbacks.ctcp_version[lfrom] > 0 then say(from, c("VERSION"))
563 else icallbacks.ctcp_version[lfrom] = nil
569 function ctcp_handlers.on_rpl_errmsg(from, to, message)
570 callback("ctcp_error", from, to, message)
575 function ctcp_handlers.on_rpl_ping(from, to, timestamp)
576 local lfrom = from:lower()
577 local cb = table.remove(icallbacks.ctcp_ping[lfrom], 1)
578 cb({time = os.time() - timestamp, nick = from})
579 if #icallbacks.ctcp_ping[lfrom] > 0 then say(from, c("PING", os.time()))
580 else icallbacks.ctcp_ping[lfrom] = nil
586 function ctcp_handlers.on_rpl_time(from, to, time)
587 local lfrom = from:lower()
588 local cb = table.remove(icallbacks.ctcp_time[lfrom], 1)
589 cb({time = time, nick = from})
590 if #icallbacks.ctcp_time[lfrom] > 0 then say(from, c("TIME"))
591 else icallbacks.ctcp_time[lfrom] = nil
599 -- module functions {{{
600 -- socket handling functions {{{
601 -- _register_socket {{{
603 -- Register a socket to listen on.
604 -- @param sock LuaSocket socket object
605 -- @param mode 'r' if the socket is for reading, 'w' if for writing
606 -- @param cb Callback to call when the socket is ready for reading/writing.
607 -- It will be called with the socket as the single argument.
608 function _register_socket(sock, mode, cb)
617 base.assert(not cbs[sock], "socket already registered")
618 table.insert(socks, sock)
623 -- _unregister_socket {{{
625 -- Remove a previously registered socket.
626 -- @param sock Socket to unregister
627 -- @param mode 'r' to unregister it for reading, 'w' for writing
628 function _unregister_socket(sock, mode)
637 for i, v in base.ipairs(socks) do
638 if v == sock then table.remove(socks, i); break; end
646 -- public functions {{{
647 -- server commands {{{
650 -- Start a connection to the irc server.
651 -- @param args Table of named arguments containing connection parameters.
652 -- Defaults are the all-caps versions of these parameters given
653 -- at the top of the file, and are overridable by setting them
654 -- as well, i.e. <pre>irc.NETWORK = irc.freenode.net</pre>
655 -- Possible options are:
657 -- <li><i>network:</i> address of the irc network to connect to
658 -- (default: 'localhost')</li>
659 -- <li><i>port:</i> port to connect to
660 -- (default: '6667')</li>
661 -- <li><i>pass:</i> irc server password
662 -- (default: don't send)</li>
663 -- <li><i>nick:</i> nickname to connect as
664 -- (default: 'luabot')</li>
665 -- <li><i>username:</i> username to connect with
666 -- (default: 'LuaIRC')</li>
667 -- <li><i>realname:</i> realname to connect with
668 -- (default: 'LuaIRC')</li>
669 -- <li><i>timeout:</i> amount of time in seconds to wait before
670 -- dropping an idle connection
671 -- (default: '60')</li>
673 function connect(args)
674 local network = args.network or NETWORK
675 local port = args.port or PORT
676 local nick = args.nick or NICK
677 local username = args.username or USERNAME
678 local realname = args.realname or REALNAME
679 local timeout = args.timeout or TIMEOUT
680 serverinfo.connecting = true
681 if OUTFILE then irc_debug.set_output(OUTFILE) end
682 if DEBUG then irc_debug.enable() end
683 irc_sock = base.assert(socket.connect(network, port))
684 irc_sock:settimeout(timeout)
685 _register_socket(irc_sock, 'r', incoming_message)
686 if args.pass then send("PASS", args.pass) end
688 send("USER", username, get_ip(), network, realname)
695 -- Close the connection to the irc server.
696 -- @param message Quit message (optional, defaults to 'Leaving')
697 function quit(message)
698 message = message or "Leaving"
699 send("QUIT", message)
700 serverinfo.connected = false
707 -- @param channel Channel to join
708 function join(channel)
709 if not channel then return end
710 serverinfo.channels[channel] = Channel.new(channel)
711 send("JOIN", channel)
718 -- @param channel Channel to leave
719 function part(channel)
720 if not channel then return end
721 serverinfo.channels[channel] = nil
722 send("PART", channel)
728 -- Send a message to a user or channel.
729 -- @param name User or channel to send the message to
730 -- @param message Message to send
731 function say(name, message)
732 if not name then return end
733 message = message or ""
734 send("PRIVMSG", name, message)
740 -- Send a notice to a user or channel.
741 -- @param name User or channel to send the notice to
742 -- @param message Message to send
743 function notice(name, message)
744 if not name then return end
745 message = message or ""
746 send("NOTICE", name, message)
752 -- Perform a /me action.
753 -- @param name User or channel to send the action to
754 -- @param action Action to send
755 function act(name, action)
756 if not name then return end
757 action = action or ""
758 send("PRIVMSG", name, c("ACTION", action))
763 -- information requests {{{
764 -- server_version {{{
766 -- Request the version of the IRC server you are currently connected to.
767 -- @param cb Callback to call when the information is available. The single
768 -- table parameter to this callback will contain the fields:
770 -- <li><i>server:</i> the server which responded to the request</li>
771 -- <li><i>version:</i> the server version</li>
772 -- <li><i>comments:</i> other data provided by the server</li>
774 function server_version(cb)
775 -- apparently the optional server parameter isn't supported for servers
776 -- which you are not directly connected to (freenode specific?)
777 local server = serverinfo.host
778 if not icallbacks.serverversion[server] then
779 icallbacks.serverversion[server] = {cb}
780 send("VERSION", server)
782 table.insert(icallbacks.serverversion[server], cb)
788 -- TODO: allow server parameter (to get user idle time)
790 -- Request WHOIS information about a given user.
791 -- @param cb Callback to call when the information is available. The single
792 -- table parameter to this callback may contain any or all of the
795 -- <li><i>nick:</i> the nick that was passed to this function
796 -- (this field will always be here)</li>
797 -- <li><i>user:</i> the IRC username of the user</li>
798 -- <li><i>host:</i> the user's hostname</li>
799 -- <li><i>realname:</i> the IRC realname of the user</li>
800 -- <li><i>server:</i> the IRC server the user is connected to</li>
801 -- <li><i>serverinfo:</i> arbitrary information about the above
803 -- <li><i>awaymsg:</i> set to the user's away message if they are
805 -- <li><i>is_oper:</i> true if the user is an IRCop</li>
806 -- <li><i>idle_time:</i> amount of time the user has been idle</li>
807 -- <li><i>channels:</i> array containing the channels the user has
810 -- @param nick User to request WHOIS information about
811 function whois(cb, nick)
813 requestinfo.whois[nick] = {}
814 if not icallbacks.whois[nick] then
815 icallbacks.whois[nick] = {cb}
818 table.insert(icallbacks.whois[nick], cb)
825 -- Request the current time of the server you are connected to.
826 -- @param cb Callback to call when the information is available. The single
827 -- table parameter to this callback will contain the fields:
829 -- <li><i>server:</i> the server which responded to the request</li>
830 -- <li><i>time:</i> the time reported by the server</li>
832 function server_time(cb)
833 -- apparently the optional server parameter isn't supported for servers
834 -- which you are not directly connected to (freenode specific?)
835 local server = serverinfo.host
836 if not icallbacks.servertime[server] then
837 icallbacks.servertime[server] = {cb}
840 table.insert(icallbacks.servertime[server], cb)
849 -- Send a CTCP ping request.
850 -- @param cb Callback to call when the information is available. The single
851 -- table parameter to this callback will contain the fields:
853 -- <li><i>nick:</i> the nick which responded to the request</li>
854 -- <li><i>time:</i> the roundtrip ping time, in seconds</li>
856 -- @param nick User to ping
857 function ctcp_ping(cb, nick)
859 if not icallbacks.ctcp_ping[nick] then
860 icallbacks.ctcp_ping[nick] = {cb}
861 say(nick, c("PING", os.time()))
863 table.insert(icallbacks.ctcp_ping[nick], cb)
870 -- Send a localtime request.
871 -- @param cb Callback to call when the information is available. The single
872 -- table parameter to this callback will contain the fields:
874 -- <li><i>nick:</i> the nick which responded to the request</li>
875 -- <li><i>time:</i> the localtime reported by the remote client</li>
877 -- @param nick User to request the localtime from
878 function ctcp_time(cb, nick)
880 if not icallbacks.ctcp_time[nick] then
881 icallbacks.ctcp_time[nick] = {cb}
884 table.insert(icallbacks.ctcp_time[nick], cb)
891 -- Send a client version request.
892 -- @param cb Callback to call when the information is available. The single
893 -- table parameter to this callback will contain the fields:
895 -- <li><i>nick:</i> the nick which responded to the request</li>
896 -- <li><i>version:</i> the version reported by the remote client</li>
898 -- @param nick User to request the client version from
899 function ctcp_version(cb, nick)
901 if not icallbacks.ctcp_version[nick] then
902 icallbacks.ctcp_version[nick] = {cb}
903 say(nick, c("VERSION"))
905 table.insert(icallbacks.ctcp_version[nick], cb)
911 -- callback functions {{{
912 -- register_callback {{{
914 -- Register a user function to be called when a specific event occurs.
915 -- @param name Name of the event
916 -- @param fn Function to call when the event occurs, or nil to clear the
917 -- callback for this event
918 -- @return Value of the original callback for this event (or nil if no previous
919 -- callback had been set)
920 function register_callback(name, fn)
921 local old_handler = user_handlers[name]
922 user_handlers[name] = fn
928 -- misc functions {{{
930 -- TODO: CTCP quoting should be explicit, this table thing is quite ugly (if
933 -- Send a raw IRC command.
934 -- @param command String containing the raw IRC command
935 -- @param ... Arguments to the command. Each argument is either a string or
936 -- an array. Strings are sent literally, arrays are CTCP quoted
937 -- as a group. The last argument (if it exists) is preceded by
938 -- a : (so it may contain spaces).
939 function send(command, ...)
940 if not serverinfo.connected and not serverinfo.connecting then return end
941 local message = command
942 for i, v in base.ipairs({...}) do
946 message = message .. " " .. v
948 message = ctcp._low_quote(message)
949 -- we just truncate for now. -2 to account for the \r\n
950 message = message:sub(1, constants.IRC_MAX_MSG - 2)
951 irc_debug._message("SEND", message)
952 irc_sock:send(message .. "\r\n")
958 -- Get the local IP address for the server connection.
959 -- @return A string representation of the local IP address that the IRC server
960 -- connection is communicating on
962 return (ip or irc_sock:getsockname())
968 -- Set the local IP manually (to allow for NAT workarounds)
969 -- @param new_ip IP address to set
970 function set_ip(new_ip)
976 -- TODO: @see doesn't currently work for files/modules
978 -- Iterate over currently joined channels.
979 -- channels() is an iterator function for use in for loops.
980 -- For example, <pre>for chan in irc.channels() do print(chan:name) end</pre>
983 return function(state, arg)
984 return misc._value_iter(state, arg,
986 return v.join_complete