]> git.lizzy.rs Git - luairc.git/blob - src/irc.lua
document the server commands
[luairc.git] / src / irc.lua
1 ---
2 -- Implementation of the main LuaIRC module
3
4 -- initialization {{{
5 local base =      _G
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'
14 -- }}}
15
16 ---
17 -- LuaIRC - IRC framework written in Lua
18 -- @release 0.02
19 module 'irc'
20
21 -- constants {{{
22 _VERSION = 'LuaIRC 0.2'
23 -- }}}
24
25 -- classes {{{
26 local Channel = base.require 'irc.channel'
27 -- }}}
28
29 -- local variables {{{
30 local irc_sock = nil
31 local rsockets = {}
32 local wsockets = {}
33 local rcallbacks = {}
34 local wcallbacks = {}
35 local icallbacks = {
36     whois = {},
37     serverversion = {},
38     servertime = {},
39     ctcp_ping = {},
40     ctcp_time = {},
41     ctcp_version = {},
42 }
43 local requestinfo = {whois = {}}
44 local handlers = {}
45 local ctcp_handlers = {}
46 local serverinfo = {}
47 -- }}}
48
49 -- defaults {{{
50 TIMEOUT = 60          -- connection timeout
51 NETWORK = "localhost" -- default network
52 PORT = 6667           -- default port
53 NICK = "luabot"       -- default nick
54 USERNAME = "LuaIRC"   -- default username
55 REALNAME = "LuaIRC"   -- default realname
56 DEBUG = false         -- whether we want extra debug information
57 OUTFILE = nil         -- file to send debug output to - nil is stdout
58 -- }}}
59
60 -- private functions {{{
61 -- main_loop_iter {{{
62 local function main_loop_iter()
63     if #rsockets == 0 and #wsockets == 0 then return false end
64     local rready, wready, err = socket.select(rsockets, wsockets)
65     if err then irc_debug.err(err); return false; end
66
67     for _, sock in base.ipairs(rready) do
68         local cb = socket.protect(rcallbacks[sock])
69         local ret, err = cb(sock)
70         if not ret then
71             irc_debug.warn("socket error: " .. err)
72             _unregister_socket(sock, 'r')
73         end
74     end
75
76     for _, sock in base.ipairs(wready) do
77         local cb = socket.protect(wcallbacks[sock])
78         local ret, err = cb(sock)
79         if not ret then
80             irc_debug.warn("socket error: " .. err)
81             _unregister_socket(sock, 'w')
82         end
83     end
84
85     return true
86 end
87 -- }}}
88
89 -- begin_main_loop {{{
90 local function begin_main_loop()
91     while main_loop_iter() do end
92 end
93 -- }}}
94
95 -- incoming_message {{{
96 local function incoming_message(sock)
97     local raw_msg = socket.try(sock:receive())
98     irc_debug.message("RECV", raw_msg)
99     local msg = message.parse(raw_msg)
100     misc.try_call_warn("Unhandled server message: " .. msg.command,
101                        handlers["on_" .. msg.command:lower()],
102                        (misc.parse_user(msg.from)), base.unpack(msg.args))
103     return true
104 end
105 -- }}}
106 -- }}}
107
108 -- internal message handlers {{{
109 -- command handlers {{{
110 -- on_nick {{{
111 function handlers.on_nick(from, new_nick)
112     for chan in channels() do
113         chan:change_nick(from, new_nick)
114     end
115     misc.try_call(on_nick_change, new_nick, from)
116 end
117 -- }}}
118
119 -- on_join {{{
120 function handlers.on_join(from, chan)
121     base.assert(serverinfo.channels[chan],
122                 "Received join message for unknown channel: " .. chan)
123     if serverinfo.channels[chan].join_complete then
124         serverinfo.channels[chan]:add_user(from)
125         misc.try_call(on_join, serverinfo.channels[chan], from)
126     end
127 end
128 -- }}}
129
130 -- on_part {{{
131 function handlers.on_part(from, chan, part_msg)
132     -- don't assert on chan here, since we get part messages for ourselves
133     -- after we remove the channel from the channel list
134     if not serverinfo.channels[chan] then return end
135     if serverinfo.channels[chan].join_complete then
136         serverinfo.channels[chan]:remove_user(from)
137         misc.try_call(on_part, serverinfo.channels[chan], from, part_msg)
138     end
139 end
140 -- }}}
141
142 -- on_mode {{{
143 function handlers.on_mode(from, to, mode_string, ...)
144     local dir = mode_string:sub(1, 1)
145     mode_string = mode_string:sub(2)
146     local args = {...}
147
148     if to:sub(1, 1) == "#" then
149         -- handle channel mode requests {{{
150         base.assert(serverinfo.channels[to],
151                     "Received mode change for unknown channel: " .. to)
152         local chan = serverinfo.channels[to]
153         local ind = 1
154         for i = 1, mode_string:len() do
155             local mode = mode_string:sub(i, i)
156             local target = args[ind]
157             -- channel modes other than op/voice will be implemented as
158             -- information request commands
159             if mode == "o" then -- channel op {{{
160                 chan:change_status(target, dir == "+", "o")
161                 misc.try_call(({["+"] = on_op, ["-"] = on_deop})[dir],
162                               chan, from, target)
163                 ind = ind + 1
164                 -- }}}
165             elseif mode == "v" then -- voice {{{
166                 chan:change_status(target, dir == "+", "v")
167                 misc.try_call(({["+"] = on_voice, ["-"] = on_devoice})[dir],
168                               chan, from, target)
169                 ind = ind + 1
170                 -- }}}
171             end
172         end
173         -- }}}
174     elseif from == to then
175         -- handle user mode requests {{{
176         -- TODO: make users more easily accessible so this is actually
177         -- reasonably possible
178         for i = 1, mode_string:len() do
179             local mode = mode_string:sub(i, i)
180             if mode == "i" then -- invisible {{{
181                 -- }}}
182             elseif mode == "s" then -- server messages {{{
183                 -- }}}
184             elseif mode == "w" then -- wallops messages {{{
185                 -- }}}
186             elseif mode == "o" then -- ircop {{{
187                 -- }}}
188             end
189         end
190         -- }}}
191     end
192 end
193 -- }}}
194
195 -- on_topic {{{
196 function handlers.on_topic(from, chan, new_topic)
197     base.assert(serverinfo.channels[chan],
198                 "Received topic message for unknown channel: " .. chan)
199     serverinfo.channels[chan]._topic.text = new_topic
200     serverinfo.channels[chan]._topic.user = (misc.parse_user(from))
201     serverinfo.channels[chan]._topic.time = os.time()
202     if serverinfo.channels[chan].join_complete then
203         misc.try_call(on_topic_change, serverinfo.channels[chan])
204     end
205 end
206 -- }}}
207
208 -- on_invite {{{
209 function handlers.on_invite(from, to, chan)
210     misc.try_call(on_invite, from, chan)
211 end
212 -- }}}
213
214 -- on_kick {{{
215 function handlers.on_kick(from, chan, to)
216     base.assert(serverinfo.channels[chan],
217                 "Received kick message for unknown channel: " .. chan)
218     if serverinfo.channels[chan].join_complete then
219         serverinfo.channels[chan]:remove_user(to)
220         misc.try_call(on_kick, serverinfo.channels[chan], to, from)
221     end
222 end
223 -- }}}
224
225 -- on_privmsg {{{
226 function handlers.on_privmsg(from, to, msg)
227     local msgs = ctcp.ctcp_split(msg, true)
228     for _, v in base.ipairs(msgs) do
229         if base.type(v) == "string" then
230             -- normal message {{{
231             if to:sub(1, 1) == "#" then
232                 base.assert(serverinfo.channels[to],
233                             "Received channel msg from unknown channel: " .. to)
234                 misc.try_call(on_channel_msg, serverinfo.channels[to], from, v)
235             else
236                 misc.try_call(on_private_msg, from, v)
237             end
238             -- }}}
239         elseif base.type(v) == "table" then
240             -- ctcp message {{{
241             local words = misc.split(v[1])
242             local received_command = words[1]
243             local cb = "on_" .. received_command:lower()
244             table.remove(words, 1)
245             -- not using try_call here because the ctcp specification requires
246             -- an error response to nonexistant commands
247             if base.type(ctcp_handlers[cb]) == "function" then
248                 ctcp_handlers[cb](from, to, table.concat(words, " "))
249             else
250                 notice(from, {"ERRMSG Unknown query: " .. received_command})
251             end
252             -- }}}
253         end
254     end
255 end
256 -- }}}
257
258 -- on_notice {{{
259 function handlers.on_notice(from, to, msg)
260     local msgs = ctcp.ctcp_split(msg, true)
261     for _, v in base.ipairs(msgs) do
262         if base.type(v) == "string" then
263             -- normal message {{{
264             if to:sub(1, 1) == "#" then
265                 base.assert(serverinfo.channels[to],
266                             "Received channel msg from unknown channel: " .. to)
267                 misc.try_call(on_channel_notice, serverinfo.channels[to],
268                               from, v)
269             else
270                 misc.try_call(on_private_notice, from, v)
271             end
272             -- }}}
273         elseif base.type(v) == "table" then
274             -- ctcp message {{{
275             local words = misc.split(v[1])
276             local command = words[1]:lower()
277             table.remove(words, 1)
278             misc.try_call_warn("Unknown CTCP message: " .. command,
279                                ctcp_handlers["on_rpl_"..command], from, to,
280                                table.concat(words, ' '))
281             -- }}}
282         end
283     end
284 end
285 -- }}}
286
287 -- on_quit {{{
288 function handlers.on_quit(from, quit_msg)
289     for name, chan in base.pairs(serverinfo.channels) do
290         chan:remove_user(from)
291     end
292     misc.try_call(on_quit, from, quit_msg)
293 end
294 -- }}}
295
296 -- on_ping {{{
297 -- respond to server pings to make sure it knows we are alive
298 function handlers.on_ping(from, respond_to)
299     send("PONG", respond_to)
300 end
301 -- }}}
302 -- }}}
303
304 -- server replies {{{
305 -- on_rpl_topic {{{
306 -- catch topic changes
307 function handlers.on_rpl_topic(from, chan, topic)
308     base.assert(serverinfo.channels[chan],
309                 "Received topic information about unknown channel: " .. chan)
310     serverinfo.channels[chan]._topic.text = topic
311 end
312 -- }}}
313
314 -- on_rpl_notopic {{{
315 function handlers.on_rpl_notopic(from, chan)
316     base.assert(serverinfo.channels[chan],
317                 "Received topic information about unknown channel: " .. chan)
318     serverinfo.channels[chan]._topic.text = ""
319 end
320 -- }}}
321
322 -- on_rpl_topicdate {{{
323 -- "topic was set by <user> at <time>"
324 function handlers.on_rpl_topicdate(from, chan, user, time)
325     base.assert(serverinfo.channels[chan],
326                 "Received topic information about unknown channel: " .. chan)
327     serverinfo.channels[chan]._topic.user = user
328     serverinfo.channels[chan]._topic.time = base.tonumber(time)
329 end
330 -- }}}
331
332 -- on_rpl_namreply {{{
333 -- handles a NAMES reply
334 function handlers.on_rpl_namreply(from, chanmode, chan, userlist)
335     base.assert(serverinfo.channels[chan],
336                 "Received user information about unknown channel: " .. chan)
337     serverinfo.channels[chan]._chanmode = constants.chanmodes[chanmode]
338     local users = misc.split(userlist)
339     for k,v in base.ipairs(users) do
340         if v:sub(1, 1) == "@" or v:sub(1, 1) == "+" then
341             local nick = v:sub(2)
342             serverinfo.channels[chan]:add_user(nick, v:sub(1, 1))
343         else
344             serverinfo.channels[chan]:add_user(v)
345         end
346     end
347 end
348 -- }}}
349
350 -- on_rpl_endofnames {{{
351 -- when we get this message, the channel join has completed, so call the
352 -- external cb
353 function handlers.on_rpl_endofnames(from, chan)
354     base.assert(serverinfo.channels[chan],
355                 "Received user information about unknown channel: " .. chan)
356     if not serverinfo.channels[chan].join_complete then
357         misc.try_call(on_me_join, serverinfo.channels[chan])
358         serverinfo.channels[chan].join_complete = true
359     end
360 end
361 -- }}}
362
363 -- on_rpl_welcome {{{
364 function handlers.on_rpl_welcome(from)
365     serverinfo = {
366         connected = false,
367         connecting = true,
368         channels = {}
369     }
370 end
371 -- }}}
372
373 -- on_rpl_yourhost {{{
374 function handlers.on_rpl_yourhost(from, msg)
375     serverinfo.host = from
376 end
377 -- }}}
378
379 -- on_rpl_motdstart {{{
380 function handlers.on_rpl_motdstart(from)
381     serverinfo.motd = ""
382 end
383 -- }}}
384
385 -- on_rpl_motd {{{
386 function handlers.on_rpl_motd(from, motd)
387     serverinfo.motd = (serverinfo.motd or "") .. motd .. "\n"
388 end
389 -- }}}
390
391 -- on_rpl_endofmotd {{{
392 function handlers.on_rpl_endofmotd(from)
393     if not serverinfo.connected then
394         serverinfo.connected = true
395         serverinfo.connecting = false
396         misc.try_call(on_connect)
397     end
398 end
399 -- }}}
400
401 -- on_rpl_whoisuser {{{
402 function handlers.on_rpl_whoisuser(from, nick, user, host, star, realname)
403     nick = nick:lower()
404     requestinfo.whois[nick].user = user
405     requestinfo.whois[nick].host = host
406     requestinfo.whois[nick].realname = realname
407 end
408 -- }}}
409
410 -- on_rpl_whoischannels {{{
411 function handlers.on_rpl_whoischannels(from, nick, channel_list)
412     nick = nick:lower()
413     if not requestinfo.whois[nick].channels then
414         requestinfo.whois[nick].channels = {}
415     end
416     for _, channel in base.ipairs(misc.split(channel_list)) do
417         table.insert(requestinfo.whois[nick].channels, channel)
418     end
419 end
420 -- }}}
421
422 -- on_rpl_whoisserver {{{
423 function handlers.on_rpl_whoisserver(from, nick, server, serverinfo)
424     nick = nick:lower()
425     requestinfo.whois[nick].server = server
426     requestinfo.whois[nick].serverinfo = serverinfo
427 end
428 -- }}}
429
430 -- on_rpl_away {{{
431 function handlers.on_rpl_away(from, nick, away_msg)
432     nick = nick:lower()
433     if requestinfo.whois[nick] then
434         requestinfo.whois[nick].away_msg = away_msg
435     end
436 end
437 -- }}}
438
439 -- on_rpl_whoisoperator {{{
440 function handlers.on_rpl_whoisoperator(from, nick)
441     requestinfo.whois[nick:lower()].is_oper = true
442 end
443 -- }}}
444
445 -- on_rpl_whoisidle {{{
446 function handlers.on_rpl_whoisidle(from, nick, idle_seconds)
447     requestinfo.whois[nick:lower()].idle_time = idle_seconds
448 end
449 -- }}}
450
451 -- on_rpl_endofwhois {{{
452 function handlers.on_rpl_endofwhois(from, nick)
453     nick = nick:lower()
454     local cb = table.remove(icallbacks.whois[nick], 1)
455     cb(requestinfo.whois[nick])
456     requestinfo.whois[nick] = nil
457     if #icallbacks.whois[nick] > 0 then send("WHOIS", nick)
458     else icallbacks.whois[nick] = nil
459     end
460 end
461 -- }}}
462
463 -- on_rpl_version {{{
464 function handlers.on_rpl_version(from, version, server, comments)
465     local cb = table.remove(icallbacks.serverversion[server], 1)
466     cb({version = version, server = server, comments = comments})
467     if #icallbacks.serverversion[server] > 0 then send("VERSION", server)
468     else icallbacks.serverversion[server] = nil
469     end
470 end
471 -- }}}
472
473 -- on_rpl_time {{{
474 function on_rpl_time(from, server, time)
475     local cb = table.remove(icallbacks.servertime[server], 1)
476     cb({time = time, server = server})
477     if #icallbacks.servertime[server] > 0 then send("TIME", server)
478     else icallbacks.servertime[server] = nil
479     end
480 end
481 -- }}}
482 -- }}}
483
484 -- ctcp handlers {{{
485 -- requests {{{
486 -- on_action {{{
487 function ctcp_handlers.on_action(from, to, message)
488     if to:sub(1, 1) == "#" then
489         base.assert(serverinfo.channels[to],
490         "Received channel msg from unknown channel: " .. to)
491         misc.try_call(on_channel_act, serverinfo.channels[to], from, message)
492     else
493         misc.try_call(on_private_act, from, message)
494     end
495 end
496 -- }}}
497
498 -- on_dcc {{{
499 function ctcp_handlers.on_dcc(from, to, message)
500     local type, argument, address, port, size = base.unpack(misc.split(message, " ", nil, '"', '"'))
501     if type == "SEND" then
502         if misc.try_call(on_dcc, from, to, argument, address, port, size) then
503             dcc.accept(argument, address, port, size)
504         end
505     elseif type == "CHAT" then
506         -- TODO: implement this? do people ever use this?
507     end
508 end
509 -- }}}
510
511 -- on_version {{{
512 function ctcp_handlers.on_version(from, to)
513     notice(from, {"VERSION " .. _VERSION .. " running under " .. base._VERSION .. " with " .. socket._VERSION})
514 end
515 -- }}}
516
517 -- on_errmsg {{{
518 function ctcp_handlers.on_errmsg(from, to, message)
519     notice(from, {"ERRMSG " .. message .. "No error has occurred"})
520 end
521 -- }}}
522
523 -- on_ping {{{
524 function ctcp_handlers.on_ping(from, to, timestamp)
525     notice(from, {"PING " .. timestamp})
526 end
527 -- }}}
528
529 -- on_time {{{
530 function ctcp_handlers.on_time(from, to)
531     notice(from, {"TIME " .. os.date()})
532 end
533 -- }}}
534 -- }}}
535
536 -- responses {{{
537 -- on_rpl_action {{{
538 -- actions are handled the same, notice or not
539 ctcp_handlers.on_rpl_action = ctcp_handlers.on_action
540 -- }}}
541
542 -- on_rpl_version {{{
543 function ctcp_handlers.on_rpl_version(from, to, version)
544     local cb = table.remove(icallbacks.ctcp_version[from], 1)
545     cb({version = version, nick = from})
546     if #icallbacks.ctcp_version[from] > 0 then say(from, {"VERSION"})
547     else icallbacks.ctcp_version[from] = nil
548     end
549 end
550 -- }}}
551
552 -- on_rpl_errmsg {{{
553 function ctcp_handlers.on_rpl_errmsg(from, to, message)
554     try_call(on_ctcp_error, from, to, message)
555 end
556 -- }}}
557
558 -- on_rpl_ping {{{
559 function ctcp_handlers.on_rpl_ping(from, to, timestamp)
560     local cb = table.remove(icallbacks.ctcp_ping[from], 1)
561     cb({time = os.time() - timestamp, nick = from})
562     if #icallbacks.ctcp_ping[from] > 0 then say(from, {"PING " .. os.time()})
563     else icallbacks.ctcp_ping[from] = nil
564     end
565 end
566 -- }}}
567
568 -- on_rpl_time {{{
569 function ctcp_handlers.on_rpl_time(from, to, time)
570     local cb = table.remove(icallbacks.ctcp_time[from], 1)
571     cb({time = time, nick = from})
572     if #icallbacks.ctcp_time[from] > 0 then say(from, {"TIME"})
573     else icallbacks.ctcp_time[from] = nil
574     end
575 end
576 -- }}}
577 -- }}}
578 -- }}}
579 -- }}}
580
581 -- module functions {{{
582 -- socket handling functions {{{
583 -- _register_socket() - register a socket to listen on {{{
584 function _register_socket(sock, mode, cb)
585     local socks, cbs
586     if mode == 'r' then
587         socks = rsockets
588         cbs = rcallbacks
589     else
590         socks = wsockets
591         cbs = wcallbacks
592     end
593     base.assert(not cbs[sock], "socket already registered")
594     table.insert(socks, sock)
595     cbs[sock] = cb
596 end
597 -- }}}
598
599 -- _unregister_socket() - remove a previously registered socket {{{
600 function _unregister_socket(sock, mode)
601     local socks, cbs
602     if mode == 'r' then
603         socks = rsockets
604         cbs = rcallbacks
605     else
606         socks = wsockets
607         cbs = wcallbacks
608     end
609     for i, v in base.ipairs(socks) do
610         if v == sock then table.remove(socks, i); break; end
611     end
612     cbs[sock] = nil
613 end
614 -- }}}
615 -- }}}
616 -- }}}
617
618 -- public functions {{{
619 -- server commands {{{
620 -- connect {{{
621 ---
622 -- Start a connection to the irc server.
623 -- @param args Table of named arguments containing connection parameters.
624 --             Defaults are the all-caps versions of these parameters given
625 --             at the top of the file, and are overridable by setting them
626 --             as well, i.e. <pre>irc.NETWORK = irc.freenode.net</pre>
627 --             Possible options are:
628 --             <ul>
629 --             <li><i>network:</i>  address of the irc network to connect to
630 --                                  (default: 'localhost')</li>
631 --             <li><i>port:</i>     port to connect to
632 --                                  (default: '6667')</li>
633 --             <li><i>pass:</i>     irc server password
634 --                                  (default: don't send)</li>
635 --             <li><i>nick:</i>     nickname to connect as
636 --                                  (default: 'luabot')</li>
637 --             <li><i>username:</i> username to connect with
638 --                                  (default: 'LuaIRC')</li>
639 --             <li><i>realname:</i> realname to connect with
640 --                                  (default: 'LuaIRC')</li>
641 --             <li><i>timeout:</i>  amount of time in seconds to wait before
642 --                                  dropping an idle connection
643 --                                  (default: '60')</li>
644 --             </ul>
645 function connect(args)
646     local network = args.network or NETWORK
647     local port = args.port or PORT
648     local nick = args.nick or NICK
649     local username = args.username or USERNAME
650     local realname = args.realname or REALNAME
651     local timeout = args.timeout or TIMEOUT
652     serverinfo.connecting = true
653     if OUTFILE then irc_debug.set_output(OUTFILE) end
654     if DEBUG then irc_debug.enable() end
655     irc_sock = base.assert(socket.connect(network, port))
656     irc_sock:settimeout(timeout)
657     _register_socket(irc_sock, 'r', incoming_message)
658     if args.pass then send("PASS", args.pass) end
659     send("NICK", nick)
660     send("USER", username, (irc_sock:getsockname()), network, realname)
661     begin_main_loop()
662 end
663 -- }}}
664
665 -- quit {{{
666 ---
667 -- Close the connection to the irc server.
668 -- @param message Quit message (optional, defaults to 'Leaving')
669 function quit(message)
670     message = message or "Leaving"
671     send("QUIT", message)
672     serverinfo.connected = false
673 end
674 -- }}}
675
676 -- join {{{
677 ---
678 -- Join a channel.
679 -- @param channel Channel to join
680 function join(channel)
681     if not channel then return end
682     serverinfo.channels[channel] = Channel.new(channel)
683     send("JOIN", channel)
684 end
685 -- }}}
686
687 -- part {{{
688 ---
689 -- Leave a channel.
690 -- @param channel Channel to leave
691 function part(channel)
692     if not channel then return end
693     serverinfo.channels[channel] = nil
694     send("PART", channel)
695 end
696 -- }}}
697
698 -- say {{{
699 ---
700 -- Send a message to a user or channel.
701 -- @param name User or channel to send the message to
702 -- @param message Message to send
703 function say(name, message)
704     if not name then return end
705     message = message or ""
706     send("PRIVMSG", name, message)
707 end
708 -- }}}
709
710 -- notice {{{
711 ---
712 -- Send a notice to a user or channel.
713 -- @param name User or channel to send the notice to
714 -- @param message Message to send
715 function notice(name, message)
716     if not name then return end
717     message = message or ""
718     send("NOTICE", name, message)
719 end
720 -- }}}
721
722 -- act {{{
723 ---
724 -- Perform a /me action.
725 -- @param name User or channel to send the action to
726 -- @param action Action to send
727 function act(name, action)
728     if not name then return end
729     action = action or ""
730     send("PRIVMSG", name, {"ACTION", action})
731 end
732 -- }}}
733 -- }}}
734
735 -- information requests {{{
736 -- server_version {{{
737 function server_version(cb)
738     -- apparently the optional server parameter isn't supported for servers
739     -- which you are not directly connected to (freenode specific?)
740     local server = serverinfo.host
741     if not icallbacks.serverversion[server] then
742         icallbacks.serverversion[server] = {cb}
743         send("VERSION", server)
744     else
745         table.insert(icallbacks.serverversion[server], cb)
746     end
747 end
748 -- }}}
749
750 -- whois {{{
751 -- TODO: allow server parameter (to get user idle time)
752 function whois(cb, nick)
753     nick = nick:lower()
754     requestinfo.whois[nick] = {nick = nick}
755     if not icallbacks.whois[nick] then
756         icallbacks.whois[nick] = {cb}
757         send("WHOIS", nick)
758     else
759         table.insert(icallbacks.whois[nick], cb)
760     end
761 end
762 -- }}}
763
764 -- server_time {{{
765 function server_time(cb)
766     -- apparently the optional server parameter isn't supported for servers
767     -- which you are not directly connected to (freenode specific?)
768     local server = serverinfo.host
769     if not icallbacks.servertime[server] then
770         icallbacks.servertime[server] = {cb}
771         send("TIME", server)
772     else
773         table.insert(icallbacks.servertime[server], cb)
774     end
775 end
776 -- }}}
777 -- }}}
778
779 -- ctcp commands {{{
780 -- ctcp_ping() - send a CTCP ping request {{{
781 function ctcp_ping(cb, nick)
782     nick = nick:lower()
783     if not icallbacks.ctcp_ping[nick] then
784         icallbacks.ctcp_ping[nick] = {cb}
785         say(nick, {"PING " .. os.time()})
786     else
787         table.insert(icallbacks.ctcp_ping[nick], cb)
788     end
789 end
790 -- }}}
791
792 -- ctcp_time() - send a localtime request {{{
793 function ctcp_time(cb, nick)
794     nick = nick:lower()
795     if not icallbacks.ctcp_time[nick] then
796         icallbacks.ctcp_time[nick] = {cb}
797         say(nick, {"TIME"})
798     else
799         table.insert(icallbacks.ctcp_time[nick], cb)
800     end
801 end
802 -- }}}
803
804 -- ctcp_version() - send a client version request {{{
805 function ctcp_version(cb, nick)
806     nick = nick:lower()
807     if not icallbacks.ctcp_version[nick] then
808         icallbacks.ctcp_version[nick] = {cb}
809         say(nick, {"VERSION"})
810     else
811         table.insert(icallbacks.ctcp_version[nick], cb)
812     end
813 end
814 -- }}}
815 -- }}}
816
817 -- misc functions {{{
818 -- send() - send a raw irc command {{{
819 -- send takes a command and a variable number of arguments
820 -- if the argument is a string, it is sent literally
821 -- if the argument is a table, it is CTCP quoted
822 -- the last argument is preceded by a :
823 function send(command, ...)
824     if not serverinfo.connected and not serverinfo.connecting then return end
825     local message = command
826     for i, v in base.ipairs({...}) do
827         local arg
828         -- passing a table in as an argument means to treat that table as a
829         -- CTCP command, so quote it appropriately
830         if base.type(v) == "string" then
831             arg = v
832         elseif base.type(v) == "table" then
833             arg = ctcp.ctcp_quote(table.concat(v, " "))
834         end
835         if i == #{...} then
836             arg = ":" .. arg
837         end
838         message = message .. " " .. arg
839     end
840     message = ctcp.low_quote(message)
841     -- we just truncate for now. -2 to account for the \r\n
842     message = message:sub(1, constants.IRC_MAX_MSG - 2)
843     irc_debug.message("SEND", message)
844     irc_sock:send(message .. "\r\n")
845 end
846 -- }}}
847
848 -- get_ip() - get the local ip address for the server connection {{{
849 function get_ip()
850     return (irc_sock:getsockname())
851 end
852 -- }}}
853
854 -- channels() - iterate over currently joined channels {{{
855 function channels()
856     return function(state, arg)
857                return misc.value_iter(state, arg,
858                                       function(v)
859                                           return v.join_complete
860                                       end)
861            end,
862            serverinfo.channels,
863            nil
864 end
865 -- }}}
866 -- }}}
867 -- }}}