]> git.lizzy.rs Git - luairc.git/blob - src/irc.lua
426445833db36b235f857c906885537c6451a09a
[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() - start a connection to the irc server {{{
621 -- args: network  - address of the irc network to connect to
622 --       port     - port to connect to
623 --       pass     - irc server password (if required)
624 --       nick     - nickname to connect as
625 --       username - username to connect with
626 --       realname - realname to connect with
627 --       timeout  - amount of time in seconds to wait before dropping an idle
628 --                  connection
629 -- notes: this function uses a table and named arguments. defaults are specified
630 --        by the capitalized versions of the arguments at the top of this file.
631 --        all args are optional.
632 function connect(args)
633     local network = args.network or NETWORK
634     local port = args.port or PORT
635     local nick = args.nick or NICK
636     local username = args.username or USERNAME
637     local realname = args.realname or REALNAME
638     local timeout = args.timeout or TIMEOUT
639     serverinfo.connecting = true
640     if OUTFILE then irc_debug.set_output(OUTFILE) end
641     if DEBUG then irc_debug.enable() end
642     irc_sock = base.assert(socket.connect(network, port))
643     irc_sock:settimeout(timeout)
644     _register_socket(irc_sock, 'r', incoming_message)
645     if args.pass then send("PASS", args.pass) end
646     send("NICK", nick)
647     send("USER", username, (irc_sock:getsockname()), network, realname)
648     begin_main_loop()
649 end
650 -- }}}
651
652 -- quit() - close the connection to the irc server {{{
653 -- args: message - quit message (optional)
654 function quit(message)
655     message = message or "Leaving"
656     send("QUIT", message)
657     serverinfo.connected = false
658 end
659 -- }}}
660
661 -- join() - join a channel {{{
662 -- args: channel - channel to join (required)
663 function join(channel)
664     if not channel then return end
665     serverinfo.channels[channel] = Channel.new(channel)
666     send("JOIN", channel)
667 end
668 -- }}}
669
670 -- part() - leave a channel {{{
671 -- args: channel - channel to leave (required)
672 function part(channel)
673     if not channel then return end
674     serverinfo.channels[channel] = nil
675     send("PART", channel)
676 end
677 -- }}}
678
679 -- say() - send a message to a user or channel {{{
680 -- args: name    - user or channel to send the message to
681 --       message - message to send
682 function say(name, message)
683     if not name then return end
684     message = message or ""
685     send("PRIVMSG", name, message)
686 end
687 -- }}}
688
689 -- notice() - send a notice to a user or channel {{{
690 -- args: name    - user or channel to send the notice to
691 --       message - message to send
692 function notice(name, message)
693     if not name then return end
694     message = message or ""
695     send("NOTICE", name, message)
696 end
697 -- }}}
698
699 -- act() - perform a /me action {{{
700 -- args: name   - user or channel to send the action to
701 --       action - action to send
702 function act(name, action)
703     if not name then return end
704     action = action or ""
705     send("PRIVMSG", name, {"ACTION", action})
706 end
707 -- }}}
708 -- }}}
709
710 -- information requests {{{
711 -- server_version {{{
712 function server_version(cb)
713     -- apparently the optional server parameter isn't supported for servers
714     -- which you are not directly connected to (freenode specific?)
715     local server = serverinfo.host
716     if not icallbacks.serverversion[server] then
717         icallbacks.serverversion[server] = {cb}
718         send("VERSION", server)
719     else
720         table.insert(icallbacks.serverversion[server], cb)
721     end
722 end
723 -- }}}
724
725 -- whois {{{
726 -- TODO: allow server parameter (to get user idle time)
727 function whois(cb, nick)
728     nick = nick:lower()
729     requestinfo.whois[nick] = {nick = nick}
730     if not icallbacks.whois[nick] then
731         icallbacks.whois[nick] = {cb}
732         send("WHOIS", nick)
733     else
734         table.insert(icallbacks.whois[nick], cb)
735     end
736 end
737 -- }}}
738
739 -- server_time {{{
740 function server_time(cb)
741     -- apparently the optional server parameter isn't supported for servers
742     -- which you are not directly connected to (freenode specific?)
743     local server = serverinfo.host
744     if not icallbacks.servertime[server] then
745         icallbacks.servertime[server] = {cb}
746         send("TIME", server)
747     else
748         table.insert(icallbacks.servertime[server], cb)
749     end
750 end
751 -- }}}
752 -- }}}
753
754 -- ctcp commands {{{
755 -- ctcp_ping() - send a CTCP ping request {{{
756 function ctcp_ping(cb, nick)
757     nick = nick:lower()
758     if not icallbacks.ctcp_ping[nick] then
759         icallbacks.ctcp_ping[nick] = {cb}
760         say(nick, {"PING " .. os.time()})
761     else
762         table.insert(icallbacks.ctcp_ping[nick], cb)
763     end
764 end
765 -- }}}
766
767 -- ctcp_time() - send a localtime request {{{
768 function ctcp_time(cb, nick)
769     nick = nick:lower()
770     if not icallbacks.ctcp_time[nick] then
771         icallbacks.ctcp_time[nick] = {cb}
772         say(nick, {"TIME"})
773     else
774         table.insert(icallbacks.ctcp_time[nick], cb)
775     end
776 end
777 -- }}}
778
779 -- ctcp_version() - send a client version request {{{
780 function ctcp_version(cb, nick)
781     nick = nick:lower()
782     if not icallbacks.ctcp_version[nick] then
783         icallbacks.ctcp_version[nick] = {cb}
784         say(nick, {"VERSION"})
785     else
786         table.insert(icallbacks.ctcp_version[nick], cb)
787     end
788 end
789 -- }}}
790 -- }}}
791
792 -- misc functions {{{
793 -- send() - send a raw irc command {{{
794 -- send takes a command and a variable number of arguments
795 -- if the argument is a string, it is sent literally
796 -- if the argument is a table, it is CTCP quoted
797 -- the last argument is preceded by a :
798 function send(command, ...)
799     if not serverinfo.connected and not serverinfo.connecting then return end
800     local message = command
801     for i, v in base.ipairs({...}) do
802         local arg
803         -- passing a table in as an argument means to treat that table as a
804         -- CTCP command, so quote it appropriately
805         if base.type(v) == "string" then
806             arg = v
807         elseif base.type(v) == "table" then
808             arg = ctcp.ctcp_quote(table.concat(v, " "))
809         end
810         if i == #{...} then
811             arg = ":" .. arg
812         end
813         message = message .. " " .. arg
814     end
815     message = ctcp.low_quote(message)
816     -- we just truncate for now. -2 to account for the \r\n
817     message = message:sub(1, constants.IRC_MAX_MSG - 2)
818     irc_debug.message("SEND", message)
819     irc_sock:send(message .. "\r\n")
820 end
821 -- }}}
822
823 -- get_ip() - get the local ip address for the server connection {{{
824 function get_ip()
825     return (irc_sock:getsockname())
826 end
827 -- }}}
828
829 -- channels() - iterate over currently joined channels {{{
830 function channels()
831     return function(state, arg)
832                return misc.value_iter(state, arg,
833                                       function(v)
834                                           return v.join_complete
835                                       end)
836            end,
837            serverinfo.channels,
838            nil
839 end
840 -- }}}
841 -- }}}
842 -- }}}