]> git.lizzy.rs Git - luairc.git/blob - src/irc.lua
callback shouldn't be just "dcc" since it only happens on DCC SEND
[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 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'
16 -- }}}
17
18 ---
19 -- LuaIRC - IRC framework written in Lua
20 -- @release 0.2
21 module 'irc'
22
23 -- constants {{{
24 _VERSION = 'LuaIRC 0.2'
25 -- }}}
26
27 -- classes {{{
28 local Channel = base.require 'irc.channel'
29 -- }}}
30
31 -- local variables {{{
32 local irc_sock = nil
33 local rsockets = {}
34 local wsockets = {}
35 local rcallbacks = {}
36 local wcallbacks = {}
37 local icallbacks = {
38     whois = {},
39     serverversion = {},
40     servertime = {},
41     ctcp_ping = {},
42     ctcp_time = {},
43     ctcp_version = {},
44 }
45 local requestinfo = {whois = {}}
46 local handlers = {}
47 local ctcp_handlers = {}
48 local user_handlers = {}
49 local serverinfo = {}
50 local ip = nil
51 -- }}}
52
53 -- defaults {{{
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
62 -- }}}
63
64 -- private functions {{{
65 -- main_loop_iter {{{
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
70
71     for _, sock in base.ipairs(rready) do
72         local cb = socket.protect(rcallbacks[sock])
73         local ret, err = cb(sock)
74         if not ret then
75             irc_debug._warn("socket error: " .. err)
76             _unregister_socket(sock, 'r')
77         end
78     end
79
80     for _, sock in base.ipairs(wready) do
81         local cb = socket.protect(wcallbacks[sock])
82         local ret, err = cb(sock)
83         if not ret then
84             irc_debug._warn("socket error: " .. err)
85             _unregister_socket(sock, 'w')
86         end
87     end
88
89     return true
90 end
91 -- }}}
92
93 -- begin_main_loop {{{
94 local function begin_main_loop()
95     while main_loop_iter() do end
96 end
97 -- }}}
98
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))
107     return true
108 end
109 -- }}}
110
111 -- callback {{{
112 local function callback(name, ...)
113     return misc._try_call(user_handlers[name], ...)
114 end
115 -- }}}
116 -- }}}
117
118 -- internal message handlers {{{
119 -- command handlers {{{
120 -- on_nick {{{
121 function handlers.on_nick(from, new_nick)
122     for chan in channels() do
123         chan:_change_nick(from, new_nick)
124     end
125     callback("nick_change", new_nick, from)
126 end
127 -- }}}
128
129 -- on_join {{{
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)
136     end
137 end
138 -- }}}
139
140 -- on_part {{{
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)
148     end
149 end
150 -- }}}
151
152 -- on_mode {{{
153 function handlers.on_mode(from, to, mode_string, ...)
154     local dir = mode_string:sub(1, 1)
155     mode_string = mode_string:sub(2)
156     local args = {...}
157
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]
163         local ind = 1
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],
172                          chan, from, target)
173                 ind = ind + 1
174                 -- }}}
175             elseif mode == "v" then -- voice {{{
176                 chan:_change_status(target, dir == "+", "v")
177                 callback(({["+"] = "voice", ["-"] = "devoice"})[dir],
178                          chan, from, target)
179                 ind = ind + 1
180                 -- }}}
181             end
182         end
183         -- }}}
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 {{{
191                 -- }}}
192             elseif mode == "s" then -- server messages {{{
193                 -- }}}
194             elseif mode == "w" then -- wallops messages {{{
195                 -- }}}
196             elseif mode == "o" then -- ircop {{{
197                 -- }}}
198             end
199         end
200         -- }}}
201     end
202 end
203 -- }}}
204
205 -- on_topic {{{
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 = (misc.parse_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])
214     end
215 end
216 -- }}}
217
218 -- on_invite {{{
219 function handlers.on_invite(from, to, chan)
220     callback("invite", from, chan)
221 end
222 -- }}}
223
224 -- on_kick {{{
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)
231     end
232 end
233 -- }}}
234
235 -- on_privmsg {{{
236 function handlers.on_privmsg(from, to, msg)
237     local msgs = ctcp._ctcp_split(msg)
238     for _, v in base.ipairs(msgs) do
239         local msg = v.str
240         if v.ctcp then
241             -- ctcp message {{{
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, " "))
250             else
251                 notice(from, c("ERRMSG", received_command, ":Unknown query"))
252             end
253             -- }}}
254         else
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)
260             else
261                 callback("private_msg", from, msg)
262             end
263             -- }}}
264         end
265     end
266 end
267 -- }}}
268
269 -- on_notice {{{
270 function handlers.on_notice(from, to, msg)
271     local msgs = ctcp._ctcp_split(msg)
272     for _, v in base.ipairs(msgs) do
273         local msg = v.str
274         if v.ctcp then
275             -- ctcp message {{{
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, ' '))
282             -- }}}
283         else
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)
289             else
290                 callback("private_notice", from, msg)
291             end
292             -- }}}
293         end
294     end
295 end
296 -- }}}
297
298 -- on_quit {{{
299 function handlers.on_quit(from, quit_msg)
300     for name, chan in base.pairs(serverinfo.channels) do
301         chan:_remove_user(from)
302     end
303     callback("quit", from, quit_msg)
304 end
305 -- }}}
306
307 -- on_ping {{{
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)
311 end
312 -- }}}
313 -- }}}
314
315 -- server replies {{{
316 -- on_rpl_topic {{{
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
322 end
323 -- }}}
324
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 = ""
330 end
331 -- }}}
332
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)
340 end
341 -- }}}
342
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))
354         else
355             serverinfo.channels[chan]:_add_user(v)
356         end
357     end
358 end
359 -- }}}
360
361 -- on_rpl_endofnames {{{
362 -- when we get this message, the channel join has completed, so call the
363 -- external cb
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
370     end
371 end
372 -- }}}
373
374 -- on_rpl_welcome {{{
375 function handlers.on_rpl_welcome(from)
376     serverinfo = {
377         connected = false,
378         connecting = true,
379         channels = {}
380     }
381 end
382 -- }}}
383
384 -- on_rpl_yourhost {{{
385 function handlers.on_rpl_yourhost(from, msg)
386     serverinfo.host = from
387 end
388 -- }}}
389
390 -- on_rpl_motdstart {{{
391 function handlers.on_rpl_motdstart(from)
392     serverinfo.motd = ""
393 end
394 -- }}}
395
396 -- on_rpl_motd {{{
397 function handlers.on_rpl_motd(from, motd)
398     serverinfo.motd = (serverinfo.motd or "") .. motd .. "\n"
399 end
400 -- }}}
401
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
407         callback("connect")
408     end
409 end
410 -- }}}
411
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
419 end
420 -- }}}
421
422 -- on_rpl_whoischannels {{{
423 function handlers.on_rpl_whoischannels(from, nick, channel_list)
424     nick = nick:lower()
425     if not requestinfo.whois[nick].channels then
426         requestinfo.whois[nick].channels = {}
427     end
428     for _, channel in base.ipairs(misc._split(channel_list)) do
429         table.insert(requestinfo.whois[nick].channels, channel)
430     end
431 end
432 -- }}}
433
434 -- on_rpl_whoisserver {{{
435 function handlers.on_rpl_whoisserver(from, nick, server, serverinfo)
436     nick = nick:lower()
437     requestinfo.whois[nick].server = server
438     requestinfo.whois[nick].serverinfo = serverinfo
439 end
440 -- }}}
441
442 -- on_rpl_away {{{
443 function handlers.on_rpl_away(from, nick, away_msg)
444     nick = nick:lower()
445     if requestinfo.whois[nick] then
446         requestinfo.whois[nick].away_msg = away_msg
447     end
448 end
449 -- }}}
450
451 -- on_rpl_whoisoperator {{{
452 function handlers.on_rpl_whoisoperator(from, nick)
453     requestinfo.whois[nick:lower()].is_oper = true
454 end
455 -- }}}
456
457 -- on_rpl_whoisidle {{{
458 function handlers.on_rpl_whoisidle(from, nick, idle_seconds)
459     requestinfo.whois[nick:lower()].idle_time = idle_seconds
460 end
461 -- }}}
462
463 -- on_rpl_endofwhois {{{
464 function handlers.on_rpl_endofwhois(from, nick)
465     nick = nick:lower()
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
471     end
472 end
473 -- }}}
474
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
481     end
482 end
483 -- }}}
484
485 -- on_rpl_time {{{
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
491     end
492 end
493 -- }}}
494 -- }}}
495
496 -- ctcp handlers {{{
497 -- requests {{{
498 -- on_action {{{
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)
504     else
505         callback("private_act", from, message)
506     end
507 end
508 -- }}}
509
510 -- on_dcc {{{
511 -- TODO: can we not have this handler be registered unless the dcc module is
512 -- loaded?
513 function ctcp_handlers.on_dcc(from, to, message)
514     local type, argument, address, port, size = base.unpack(misc._split(message, " ", nil, '"', '"'))
515     if type == "SEND" then
516         if callback("dcc_send", from, to, argument, address, port, size) then
517             dcc._accept(argument, address, port)
518         end
519     elseif type == "CHAT" then
520         -- TODO: implement this? do people ever use this?
521     end
522 end
523 -- }}}
524
525 -- on_version {{{
526 function ctcp_handlers.on_version(from, to)
527     notice(from, c("VERSION", _VERSION .. " running under " .. base._VERSION .. " with " .. socket._VERSION))
528 end
529 -- }}}
530
531 -- on_errmsg {{{
532 function ctcp_handlers.on_errmsg(from, to, message)
533     notice(from, c("ERRMSG", message, ":No error has occurred"))
534 end
535 -- }}}
536
537 -- on_ping {{{
538 function ctcp_handlers.on_ping(from, to, timestamp)
539     notice(from, c("PING", timestamp))
540 end
541 -- }}}
542
543 -- on_time {{{
544 function ctcp_handlers.on_time(from, to)
545     notice(from, c("TIME", os.date()))
546 end
547 -- }}}
548 -- }}}
549
550 -- responses {{{
551 -- on_rpl_action {{{
552 -- actions are handled the same, notice or not
553 ctcp_handlers.on_rpl_action = ctcp_handlers.on_action
554 -- }}}
555
556 -- on_rpl_version {{{
557 function ctcp_handlers.on_rpl_version(from, to, version)
558     local lfrom = from:lower()
559     local cb = table.remove(icallbacks.ctcp_version[lfrom], 1)
560     cb({version = version, nick = from})
561     if #icallbacks.ctcp_version[lfrom] > 0 then say(from, c("VERSION"))
562     else icallbacks.ctcp_version[lfrom] = nil
563     end
564 end
565 -- }}}
566
567 -- on_rpl_errmsg {{{
568 function ctcp_handlers.on_rpl_errmsg(from, to, message)
569     callback("ctcp_error", from, to, message)
570 end
571 -- }}}
572
573 -- on_rpl_ping {{{
574 function ctcp_handlers.on_rpl_ping(from, to, timestamp)
575     local lfrom = from:lower()
576     local cb = table.remove(icallbacks.ctcp_ping[lfrom], 1)
577     cb({time = os.time() - timestamp, nick = from})
578     if #icallbacks.ctcp_ping[lfrom] > 0 then say(from, c("PING", os.time()))
579     else icallbacks.ctcp_ping[lfrom] = nil
580     end
581 end
582 -- }}}
583
584 -- on_rpl_time {{{
585 function ctcp_handlers.on_rpl_time(from, to, time)
586     local lfrom = from:lower()
587     local cb = table.remove(icallbacks.ctcp_time[lfrom], 1)
588     cb({time = time, nick = from})
589     if #icallbacks.ctcp_time[lfrom] > 0 then say(from, c("TIME"))
590     else icallbacks.ctcp_time[lfrom] = nil
591     end
592 end
593 -- }}}
594 -- }}}
595 -- }}}
596 -- }}}
597
598 -- module functions {{{
599 -- socket handling functions {{{
600 -- _register_socket {{{
601 --
602 -- Register a socket to listen on.
603 -- @param sock LuaSocket socket object
604 -- @param mode 'r' if the socket is for reading, 'w' if for writing
605 -- @param cb   Callback to call when the socket is ready for reading/writing.
606 --             It will be called with the socket as the single argument.
607 function _register_socket(sock, mode, cb)
608     local socks, cbs
609     if mode == 'r' then
610         socks = rsockets
611         cbs = rcallbacks
612     else
613         socks = wsockets
614         cbs = wcallbacks
615     end
616     base.assert(not cbs[sock], "socket already registered")
617     table.insert(socks, sock)
618     cbs[sock] = cb
619 end
620 -- }}}
621
622 -- _unregister_socket {{{
623 --
624 -- Remove a previously registered socket.
625 -- @param sock Socket to unregister
626 -- @param mode 'r' to unregister it for reading, 'w' for writing
627 function _unregister_socket(sock, mode)
628     local socks, cbs
629     if mode == 'r' then
630         socks = rsockets
631         cbs = rcallbacks
632     else
633         socks = wsockets
634         cbs = wcallbacks
635     end
636     for i, v in base.ipairs(socks) do
637         if v == sock then table.remove(socks, i); break; end
638     end
639     cbs[sock] = nil
640 end
641 -- }}}
642 -- }}}
643 -- }}}
644
645 -- public functions {{{
646 -- server commands {{{
647 -- connect {{{
648 ---
649 -- Start a connection to the irc server.
650 -- @param args Table of named arguments containing connection parameters.
651 --             Defaults are the all-caps versions of these parameters given
652 --             at the top of the file, and are overridable by setting them
653 --             as well, i.e. <pre>irc.NETWORK = irc.freenode.net</pre>
654 --             Possible options are:
655 --             <ul>
656 --             <li><i>network:</i>  address of the irc network to connect to
657 --                                  (default: 'localhost')</li>
658 --             <li><i>port:</i>     port to connect to
659 --                                  (default: '6667')</li>
660 --             <li><i>pass:</i>     irc server password
661 --                                  (default: don't send)</li>
662 --             <li><i>nick:</i>     nickname to connect as
663 --                                  (default: 'luabot')</li>
664 --             <li><i>username:</i> username to connect with
665 --                                  (default: 'LuaIRC')</li>
666 --             <li><i>realname:</i> realname to connect with
667 --                                  (default: 'LuaIRC')</li>
668 --             <li><i>timeout:</i>  amount of time in seconds to wait before
669 --                                  dropping an idle connection
670 --                                  (default: '60')</li>
671 --             </ul>
672 function connect(args)
673     local network = args.network or NETWORK
674     local port = args.port or PORT
675     local nick = args.nick or NICK
676     local username = args.username or USERNAME
677     local realname = args.realname or REALNAME
678     local timeout = args.timeout or TIMEOUT
679     serverinfo.connecting = true
680     if OUTFILE then irc_debug.set_output(OUTFILE) end
681     if DEBUG then irc_debug.enable() end
682     irc_sock = base.assert(socket.connect(network, port))
683     irc_sock:settimeout(timeout)
684     _register_socket(irc_sock, 'r', incoming_message)
685     if args.pass then send("PASS", args.pass) end
686     send("NICK", nick)
687     send("USER", username, get_ip(), network, realname)
688     begin_main_loop()
689 end
690 -- }}}
691
692 -- quit {{{
693 ---
694 -- Close the connection to the irc server.
695 -- @param message Quit message (optional, defaults to 'Leaving')
696 function quit(message)
697     message = message or "Leaving"
698     send("QUIT", message)
699     serverinfo.connected = false
700 end
701 -- }}}
702
703 -- join {{{
704 ---
705 -- Join a channel.
706 -- @param channel Channel to join
707 function join(channel)
708     if not channel then return end
709     serverinfo.channels[channel] = Channel.new(channel)
710     send("JOIN", channel)
711 end
712 -- }}}
713
714 -- part {{{
715 ---
716 -- Leave a channel.
717 -- @param channel Channel to leave
718 function part(channel)
719     if not channel then return end
720     serverinfo.channels[channel] = nil
721     send("PART", channel)
722 end
723 -- }}}
724
725 -- say {{{
726 ---
727 -- Send a message to a user or channel.
728 -- @param name User or channel to send the message to
729 -- @param message Message to send
730 function say(name, message)
731     if not name then return end
732     message = message or ""
733     send("PRIVMSG", name, message)
734 end
735 -- }}}
736
737 -- notice {{{
738 ---
739 -- Send a notice to a user or channel.
740 -- @param name User or channel to send the notice to
741 -- @param message Message to send
742 function notice(name, message)
743     if not name then return end
744     message = message or ""
745     send("NOTICE", name, message)
746 end
747 -- }}}
748
749 -- act {{{
750 ---
751 -- Perform a /me action.
752 -- @param name User or channel to send the action to
753 -- @param action Action to send
754 function act(name, action)
755     if not name then return end
756     action = action or ""
757     send("PRIVMSG", name, c("ACTION", action))
758 end
759 -- }}}
760 -- }}}
761
762 -- information requests {{{
763 -- server_version {{{
764 ---
765 -- Request the version of the IRC server you are currently connected to.
766 -- @param cb Callback to call when the information is available. The single
767 --           table parameter to this callback will contain the fields:
768 --           <ul>
769 --           <li><i>server:</i>   the server which responded to the request</li>
770 --           <li><i>version:</i>  the server version</li>
771 --           <li><i>comments:</i> other data provided by the server</li>
772 --           </ul>
773 function server_version(cb)
774     -- apparently the optional server parameter isn't supported for servers
775     -- which you are not directly connected to (freenode specific?)
776     local server = serverinfo.host
777     if not icallbacks.serverversion[server] then
778         icallbacks.serverversion[server] = {cb}
779         send("VERSION", server)
780     else
781         table.insert(icallbacks.serverversion[server], cb)
782     end
783 end
784 -- }}}
785
786 -- whois {{{
787 -- TODO: allow server parameter (to get user idle time)
788 ---
789 -- Request WHOIS information about a given user.
790 -- @param cb Callback to call when the information is available. The single
791 --           table parameter to this callback may contain any or all of the
792 --           fields:
793 --           <ul>
794 --           <li><i>nick:</i>       the nick that was passed to this function
795 --                                  (this field will always be here)</li>
796 --           <li><i>user:</i>       the IRC username of the user</li>
797 --           <li><i>host:</i>       the user's hostname</li>
798 --           <li><i>realname:</i>   the IRC realname of the user</li>
799 --           <li><i>server:</i>     the IRC server the user is connected to</li>
800 --           <li><i>serverinfo:</i> arbitrary information about the above
801 --                                  server</li>
802 --           <li><i>awaymsg:</i>    set to the user's away message if they are
803 --                                  away</li>
804 --           <li><i>is_oper:</i>    true if the user is an IRCop</li>
805 --           <li><i>idle_time:</i>  amount of time the user has been idle</li>
806 --           <li><i>channels:</i>   array containing the channels the user has
807 --                                  joined</li>
808 --           </ul>
809 -- @param nick User to request WHOIS information about
810 function whois(cb, nick)
811     nick = nick:lower()
812     requestinfo.whois[nick] = {}
813     if not icallbacks.whois[nick] then
814         icallbacks.whois[nick] = {cb}
815         send("WHOIS", nick)
816     else
817         table.insert(icallbacks.whois[nick], cb)
818     end
819 end
820 -- }}}
821
822 -- server_time {{{
823 ---
824 -- Request the current time of the server you are connected to.
825 -- @param cb Callback to call when the information is available. The single
826 --           table parameter to this callback will contain the fields:
827 --           <ul>
828 --           <li><i>server:</i> the server which responded to the request</li>
829 --           <li><i>time:</i>   the time reported by the server</li>
830 --           </ul>
831 function server_time(cb)
832     -- apparently the optional server parameter isn't supported for servers
833     -- which you are not directly connected to (freenode specific?)
834     local server = serverinfo.host
835     if not icallbacks.servertime[server] then
836         icallbacks.servertime[server] = {cb}
837         send("TIME", server)
838     else
839         table.insert(icallbacks.servertime[server], cb)
840     end
841 end
842 -- }}}
843 -- }}}
844
845 -- ctcp commands {{{
846 -- ctcp_ping {{{
847 ---
848 -- Send a CTCP ping request.
849 -- @param cb Callback to call when the information is available. The single
850 --           table parameter to this callback will contain the fields:
851 --           <ul>
852 --           <li><i>nick:</i> the nick which responded to the request</li>
853 --           <li><i>time:</i> the roundtrip ping time, in seconds</li>
854 --           </ul>
855 -- @param nick User to ping
856 function ctcp_ping(cb, nick)
857     nick = nick:lower()
858     if not icallbacks.ctcp_ping[nick] then
859         icallbacks.ctcp_ping[nick] = {cb}
860         say(nick, c("PING", os.time()))
861     else
862         table.insert(icallbacks.ctcp_ping[nick], cb)
863     end
864 end
865 -- }}}
866
867 -- ctcp_time {{{
868 ---
869 -- Send a localtime request.
870 -- @param cb Callback to call when the information is available. The single
871 --           table parameter to this callback will contain the fields:
872 --           <ul>
873 --           <li><i>nick:</i> the nick which responded to the request</li>
874 --           <li><i>time:</i> the localtime reported by the remote client</li>
875 --           </ul>
876 -- @param nick User to request the localtime from
877 function ctcp_time(cb, nick)
878     nick = nick:lower()
879     if not icallbacks.ctcp_time[nick] then
880         icallbacks.ctcp_time[nick] = {cb}
881         say(nick, c("TIME"))
882     else
883         table.insert(icallbacks.ctcp_time[nick], cb)
884     end
885 end
886 -- }}}
887
888 -- ctcp_version {{{
889 ---
890 -- Send a client version request.
891 -- @param cb Callback to call when the information is available. The single
892 --           table parameter to this callback will contain the fields:
893 --           <ul>
894 --           <li><i>nick:</i>    the nick which responded to the request</li>
895 --           <li><i>version:</i> the version reported by the remote client</li>
896 --           </ul>
897 -- @param nick User to request the client version from
898 function ctcp_version(cb, nick)
899     nick = nick:lower()
900     if not icallbacks.ctcp_version[nick] then
901         icallbacks.ctcp_version[nick] = {cb}
902         say(nick, c("VERSION"))
903     else
904         table.insert(icallbacks.ctcp_version[nick], cb)
905     end
906 end
907 -- }}}
908 -- }}}
909
910 -- callback functions {{{
911 -- register_callback {{{
912 ---
913 -- Register a user function to be called when a specific event occurs.
914 -- @param name Name of the event
915 -- @param fn   Function to call when the event occurs, or nil to clear the
916 --             callback for this event
917 -- @return Value of the original callback for this event (or nil if no previous
918 --         callback had been set)
919 function register_callback(name, fn)
920     local old_handler = user_handlers[name]
921     user_handlers[name] = fn
922     return old_handler
923 end
924 -- }}}
925 -- }}}
926
927 -- misc functions {{{
928 -- send {{{
929 -- TODO: CTCP quoting should be explicit, this table thing is quite ugly (if
930 -- convenient)
931 ---
932 -- Send a raw IRC command.
933 -- @param command String containing the raw IRC command
934 -- @param ...     Arguments to the command. Each argument is either a string or
935 --                an array. Strings are sent literally, arrays are CTCP quoted
936 --                as a group. The last argument (if it exists) is preceded by
937 --                a : (so it may contain spaces).
938 function send(command, ...)
939     if not serverinfo.connected and not serverinfo.connecting then return end
940     local message = command
941     for i, v in base.ipairs({...}) do
942         if i == #{...} then
943             v = ":" .. v
944         end
945         message = message .. " " .. v
946     end
947     message = ctcp._low_quote(message)
948     -- we just truncate for now. -2 to account for the \r\n
949     message = message:sub(1, constants.IRC_MAX_MSG - 2)
950     irc_debug._message("SEND", message)
951     irc_sock:send(message .. "\r\n")
952 end
953 -- }}}
954
955 -- get_ip {{{
956 ---
957 -- Get the local IP address for the server connection.
958 -- @return A string representation of the local IP address that the IRC server
959 --         connection is communicating on
960 function get_ip()
961     return (ip or irc_sock:getsockname())
962 end
963 -- }}}
964
965 -- set_ip {{{
966 ---
967 -- Set the local IP manually (to allow for NAT workarounds)
968 -- @param new_ip IP address to set
969 function set_ip(new_ip)
970     ip = new_ip
971 end
972 -- }}}
973
974 -- channels {{{
975 -- TODO: @see doesn't currently work for files/modules
976 ---
977 -- Iterate over currently joined channels.
978 -- channels() is an iterator function for use in for loops.
979 -- For example, <pre>for chan in irc.channels() do print(chan:name) end</pre>
980 -- @see irc.channel
981 function channels()
982     return function(state, arg)
983                return misc._value_iter(state, arg,
984                                        function(v)
985                                            return v.join_complete
986                                        end)
987            end,
988            serverinfo.channels,
989            nil
990 end
991 -- }}}
992 -- }}}
993 -- }}}