]> git.lizzy.rs Git - luairc.git/blob - src/irc.lua
document ctcp 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 ---
738 -- Request the version of the IRC server you are currently connected to.
739 -- @param cb Callback to call when the information is available. The single
740 --           table parameter to this callback will contain the fields:
741 --           <ul>
742 --           <li><i>server:</i>   the server which responded to the request</li>
743 --           <li><i>version:</i>  the server version</li>
744 --           <li><i>comments:</i> other data provided by the server</li>
745 --           </ul>
746 function server_version(cb)
747     -- apparently the optional server parameter isn't supported for servers
748     -- which you are not directly connected to (freenode specific?)
749     local server = serverinfo.host
750     if not icallbacks.serverversion[server] then
751         icallbacks.serverversion[server] = {cb}
752         send("VERSION", server)
753     else
754         table.insert(icallbacks.serverversion[server], cb)
755     end
756 end
757 -- }}}
758
759 -- whois {{{
760 -- TODO: allow server parameter (to get user idle time)
761 ---
762 -- Request WHOIS information about a given user.
763 -- @param cb Callback to call when the information is available. The single
764 --           table parameter to this callback may contain any or all of the
765 --           fields:
766 --           <ul>
767 --           <li><i>nick:</i>       the nick that was passed to this function
768 --                                  (this field will always be here)</li>
769 --           <li><i>user:</i>       the IRC username of the user</li>
770 --           <li><i>host:</i>       the user's hostname</li>
771 --           <li><i>realname:</i>   the IRC realname of the user</li>
772 --           <li><i>server:</i>     the IRC server the user is connected to</li>
773 --           <li><i>serverinfo:</i> arbitrary information about the above
774 --                                  server</li>
775 --           <li><i>awaymsg:</i>    set to the user's away message if they are
776 --                                  away</li>
777 --           <li><i>is_oper:</i>    true if the user is an IRCop</li>
778 --           <li><i>idle_time:</i>  amount of time the user has been idle</li>
779 --           <li><i>channels:</i>   array containing the channels the user has
780 --                                  joined</li>
781 --           </ul>
782 -- @param nick User to request WHOIS information about
783 function whois(cb, nick)
784     nick = nick:lower()
785     requestinfo.whois[nick] = {nick = nick}
786     if not icallbacks.whois[nick] then
787         icallbacks.whois[nick] = {cb}
788         send("WHOIS", nick)
789     else
790         table.insert(icallbacks.whois[nick], cb)
791     end
792 end
793 -- }}}
794
795 -- server_time {{{
796 ---
797 -- Request the current time of the server you are connected to.
798 -- @param cb Callback to call when the information is available. The single
799 --           table parameter to this callback will contain the fields:
800 --           <ul>
801 --           <li><i>server:</i> the server which responded to the request</li>
802 --           <li><i>time:</i>   the time reported by the server</li>
803 --           </ul>
804 function server_time(cb)
805     -- apparently the optional server parameter isn't supported for servers
806     -- which you are not directly connected to (freenode specific?)
807     local server = serverinfo.host
808     if not icallbacks.servertime[server] then
809         icallbacks.servertime[server] = {cb}
810         send("TIME", server)
811     else
812         table.insert(icallbacks.servertime[server], cb)
813     end
814 end
815 -- }}}
816 -- }}}
817
818 -- ctcp commands {{{
819 -- ctcp_ping {{{
820 ---
821 -- Send a CTCP ping request.
822 -- @param cb Callback to call when the information is available. The single
823 --           table parameter to this callback will contain the fields:
824 --           <ul>
825 --           <li><i>nick:</i> the nick which responded to the request</li>
826 --           <li><i>time:</i> the roundtrip ping time, in seconds</li>
827 --           </ul>
828 -- @param nick User to ping
829 function ctcp_ping(cb, nick)
830     nick = nick:lower()
831     if not icallbacks.ctcp_ping[nick] then
832         icallbacks.ctcp_ping[nick] = {cb}
833         say(nick, {"PING " .. os.time()})
834     else
835         table.insert(icallbacks.ctcp_ping[nick], cb)
836     end
837 end
838 -- }}}
839
840 -- ctcp_time {{{
841 ---
842 -- Send a localtime request.
843 -- @param cb Callback to call when the information is available. The single
844 --           table parameter to this callback will contain the fields:
845 --           <ul>
846 --           <li><i>nick:</i> the nick which responded to the request</li>
847 --           <li><i>time:</i> the localtime reported by the remote client</li>
848 --           </ul>
849 -- @param nick User to request the localtime from
850 function ctcp_time(cb, nick)
851     nick = nick:lower()
852     if not icallbacks.ctcp_time[nick] then
853         icallbacks.ctcp_time[nick] = {cb}
854         say(nick, {"TIME"})
855     else
856         table.insert(icallbacks.ctcp_time[nick], cb)
857     end
858 end
859 -- }}}
860
861 -- ctcp_version {{{
862 ---
863 -- Send a client version request.
864 -- @param cb Callback to call when the information is available. The single
865 --           table parameter to this callback will contain the fields:
866 --           <ul>
867 --           <li><i>nick:</i>    the nick which responded to the request</li>
868 --           <li><i>version:</i> the version reported by the remote client</li>
869 --           </ul>
870 -- @param nick User to request the client version from
871 function ctcp_version(cb, nick)
872     nick = nick:lower()
873     if not icallbacks.ctcp_version[nick] then
874         icallbacks.ctcp_version[nick] = {cb}
875         say(nick, {"VERSION"})
876     else
877         table.insert(icallbacks.ctcp_version[nick], cb)
878     end
879 end
880 -- }}}
881 -- }}}
882
883 -- misc functions {{{
884 -- send() - send a raw irc command {{{
885 -- send takes a command and a variable number of arguments
886 -- if the argument is a string, it is sent literally
887 -- if the argument is a table, it is CTCP quoted
888 -- the last argument is preceded by a :
889 function send(command, ...)
890     if not serverinfo.connected and not serverinfo.connecting then return end
891     local message = command
892     for i, v in base.ipairs({...}) do
893         local arg
894         -- passing a table in as an argument means to treat that table as a
895         -- CTCP command, so quote it appropriately
896         if base.type(v) == "string" then
897             arg = v
898         elseif base.type(v) == "table" then
899             arg = ctcp.ctcp_quote(table.concat(v, " "))
900         end
901         if i == #{...} then
902             arg = ":" .. arg
903         end
904         message = message .. " " .. arg
905     end
906     message = ctcp.low_quote(message)
907     -- we just truncate for now. -2 to account for the \r\n
908     message = message:sub(1, constants.IRC_MAX_MSG - 2)
909     irc_debug.message("SEND", message)
910     irc_sock:send(message .. "\r\n")
911 end
912 -- }}}
913
914 -- get_ip() - get the local ip address for the server connection {{{
915 function get_ip()
916     return (irc_sock:getsockname())
917 end
918 -- }}}
919
920 -- channels() - iterate over currently joined channels {{{
921 function channels()
922     return function(state, arg)
923                return misc.value_iter(state, arg,
924                                       function(v)
925                                           return v.join_complete
926                                       end)
927            end,
928            serverinfo.channels,
929            nil
930 end
931 -- }}}
932 -- }}}
933 -- }}}