]> git.lizzy.rs Git - luairc.git/blob - src/irc.lua
convert parse_user to an internal function, since apparently that's how i've been...
[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 = 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     address = misc._ip_int_to_str(address)
516     if type == "SEND" then
517         if callback("dcc_send", from, to, argument, address, port, size) then
518             dcc._accept(argument, address, port)
519         end
520     elseif type == "CHAT" then
521         -- TODO: implement this? do people ever use this?
522     end
523 end
524 -- }}}
525
526 -- on_version {{{
527 function ctcp_handlers.on_version(from, to)
528     notice(from, c("VERSION", _VERSION .. " running under " .. base._VERSION .. " with " .. socket._VERSION))
529 end
530 -- }}}
531
532 -- on_errmsg {{{
533 function ctcp_handlers.on_errmsg(from, to, message)
534     notice(from, c("ERRMSG", message, ":No error has occurred"))
535 end
536 -- }}}
537
538 -- on_ping {{{
539 function ctcp_handlers.on_ping(from, to, timestamp)
540     notice(from, c("PING", timestamp))
541 end
542 -- }}}
543
544 -- on_time {{{
545 function ctcp_handlers.on_time(from, to)
546     notice(from, c("TIME", os.date()))
547 end
548 -- }}}
549 -- }}}
550
551 -- responses {{{
552 -- on_rpl_action {{{
553 -- actions are handled the same, notice or not
554 ctcp_handlers.on_rpl_action = ctcp_handlers.on_action
555 -- }}}
556
557 -- on_rpl_version {{{
558 function ctcp_handlers.on_rpl_version(from, to, version)
559     local lfrom = from:lower()
560     local cb = table.remove(icallbacks.ctcp_version[lfrom], 1)
561     cb({version = version, nick = from})
562     if #icallbacks.ctcp_version[lfrom] > 0 then say(from, c("VERSION"))
563     else icallbacks.ctcp_version[lfrom] = nil
564     end
565 end
566 -- }}}
567
568 -- on_rpl_errmsg {{{
569 function ctcp_handlers.on_rpl_errmsg(from, to, message)
570     callback("ctcp_error", from, to, message)
571 end
572 -- }}}
573
574 -- on_rpl_ping {{{
575 function ctcp_handlers.on_rpl_ping(from, to, timestamp)
576     local lfrom = from:lower()
577     local cb = table.remove(icallbacks.ctcp_ping[lfrom], 1)
578     cb({time = os.time() - timestamp, nick = from})
579     if #icallbacks.ctcp_ping[lfrom] > 0 then say(from, c("PING", os.time()))
580     else icallbacks.ctcp_ping[lfrom] = nil
581     end
582 end
583 -- }}}
584
585 -- on_rpl_time {{{
586 function ctcp_handlers.on_rpl_time(from, to, time)
587     local lfrom = from:lower()
588     local cb = table.remove(icallbacks.ctcp_time[lfrom], 1)
589     cb({time = time, nick = from})
590     if #icallbacks.ctcp_time[lfrom] > 0 then say(from, c("TIME"))
591     else icallbacks.ctcp_time[lfrom] = nil
592     end
593 end
594 -- }}}
595 -- }}}
596 -- }}}
597 -- }}}
598
599 -- module functions {{{
600 -- socket handling functions {{{
601 -- _register_socket {{{
602 --
603 -- Register a socket to listen on.
604 -- @param sock LuaSocket socket object
605 -- @param mode 'r' if the socket is for reading, 'w' if for writing
606 -- @param cb   Callback to call when the socket is ready for reading/writing.
607 --             It will be called with the socket as the single argument.
608 function _register_socket(sock, mode, cb)
609     local socks, cbs
610     if mode == 'r' then
611         socks = rsockets
612         cbs = rcallbacks
613     else
614         socks = wsockets
615         cbs = wcallbacks
616     end
617     base.assert(not cbs[sock], "socket already registered")
618     table.insert(socks, sock)
619     cbs[sock] = cb
620 end
621 -- }}}
622
623 -- _unregister_socket {{{
624 --
625 -- Remove a previously registered socket.
626 -- @param sock Socket to unregister
627 -- @param mode 'r' to unregister it for reading, 'w' for writing
628 function _unregister_socket(sock, mode)
629     local socks, cbs
630     if mode == 'r' then
631         socks = rsockets
632         cbs = rcallbacks
633     else
634         socks = wsockets
635         cbs = wcallbacks
636     end
637     for i, v in base.ipairs(socks) do
638         if v == sock then table.remove(socks, i); break; end
639     end
640     cbs[sock] = nil
641 end
642 -- }}}
643 -- }}}
644 -- }}}
645
646 -- public functions {{{
647 -- server commands {{{
648 -- connect {{{
649 ---
650 -- Start a connection to the irc server.
651 -- @param args Table of named arguments containing connection parameters.
652 --             Defaults are the all-caps versions of these parameters given
653 --             at the top of the file, and are overridable by setting them
654 --             as well, i.e. <pre>irc.NETWORK = irc.freenode.net</pre>
655 --             Possible options are:
656 --             <ul>
657 --             <li><i>network:</i>  address of the irc network to connect to
658 --                                  (default: 'localhost')</li>
659 --             <li><i>port:</i>     port to connect to
660 --                                  (default: '6667')</li>
661 --             <li><i>pass:</i>     irc server password
662 --                                  (default: don't send)</li>
663 --             <li><i>nick:</i>     nickname to connect as
664 --                                  (default: 'luabot')</li>
665 --             <li><i>username:</i> username to connect with
666 --                                  (default: 'LuaIRC')</li>
667 --             <li><i>realname:</i> realname to connect with
668 --                                  (default: 'LuaIRC')</li>
669 --             <li><i>timeout:</i>  amount of time in seconds to wait before
670 --                                  dropping an idle connection
671 --                                  (default: '60')</li>
672 --             </ul>
673 function connect(args)
674     local network = args.network or NETWORK
675     local port = args.port or PORT
676     local nick = args.nick or NICK
677     local username = args.username or USERNAME
678     local realname = args.realname or REALNAME
679     local timeout = args.timeout or TIMEOUT
680     serverinfo.connecting = true
681     if OUTFILE then irc_debug.set_output(OUTFILE) end
682     if DEBUG then irc_debug.enable() end
683     irc_sock = base.assert(socket.connect(network, port))
684     irc_sock:settimeout(timeout)
685     _register_socket(irc_sock, 'r', incoming_message)
686     if args.pass then send("PASS", args.pass) end
687     send("NICK", nick)
688     send("USER", username, get_ip(), network, realname)
689     begin_main_loop()
690 end
691 -- }}}
692
693 -- quit {{{
694 ---
695 -- Close the connection to the irc server.
696 -- @param message Quit message (optional, defaults to 'Leaving')
697 function quit(message)
698     message = message or "Leaving"
699     send("QUIT", message)
700     serverinfo.connected = false
701 end
702 -- }}}
703
704 -- join {{{
705 ---
706 -- Join a channel.
707 -- @param channel Channel to join
708 function join(channel)
709     if not channel then return end
710     serverinfo.channels[channel] = Channel.new(channel)
711     send("JOIN", channel)
712 end
713 -- }}}
714
715 -- part {{{
716 ---
717 -- Leave a channel.
718 -- @param channel Channel to leave
719 function part(channel)
720     if not channel then return end
721     serverinfo.channels[channel] = nil
722     send("PART", channel)
723 end
724 -- }}}
725
726 -- say {{{
727 ---
728 -- Send a message to a user or channel.
729 -- @param name User or channel to send the message to
730 -- @param message Message to send
731 function say(name, message)
732     if not name then return end
733     message = message or ""
734     send("PRIVMSG", name, message)
735 end
736 -- }}}
737
738 -- notice {{{
739 ---
740 -- Send a notice to a user or channel.
741 -- @param name User or channel to send the notice to
742 -- @param message Message to send
743 function notice(name, message)
744     if not name then return end
745     message = message or ""
746     send("NOTICE", name, message)
747 end
748 -- }}}
749
750 -- act {{{
751 ---
752 -- Perform a /me action.
753 -- @param name User or channel to send the action to
754 -- @param action Action to send
755 function act(name, action)
756     if not name then return end
757     action = action or ""
758     send("PRIVMSG", name, c("ACTION", action))
759 end
760 -- }}}
761 -- }}}
762
763 -- information requests {{{
764 -- server_version {{{
765 ---
766 -- Request the version of the IRC server you are currently connected to.
767 -- @param cb Callback to call when the information is available. The single
768 --           table parameter to this callback will contain the fields:
769 --           <ul>
770 --           <li><i>server:</i>   the server which responded to the request</li>
771 --           <li><i>version:</i>  the server version</li>
772 --           <li><i>comments:</i> other data provided by the server</li>
773 --           </ul>
774 function server_version(cb)
775     -- apparently the optional server parameter isn't supported for servers
776     -- which you are not directly connected to (freenode specific?)
777     local server = serverinfo.host
778     if not icallbacks.serverversion[server] then
779         icallbacks.serverversion[server] = {cb}
780         send("VERSION", server)
781     else
782         table.insert(icallbacks.serverversion[server], cb)
783     end
784 end
785 -- }}}
786
787 -- whois {{{
788 -- TODO: allow server parameter (to get user idle time)
789 ---
790 -- Request WHOIS information about a given user.
791 -- @param cb Callback to call when the information is available. The single
792 --           table parameter to this callback may contain any or all of the
793 --           fields:
794 --           <ul>
795 --           <li><i>nick:</i>       the nick that was passed to this function
796 --                                  (this field will always be here)</li>
797 --           <li><i>user:</i>       the IRC username of the user</li>
798 --           <li><i>host:</i>       the user's hostname</li>
799 --           <li><i>realname:</i>   the IRC realname of the user</li>
800 --           <li><i>server:</i>     the IRC server the user is connected to</li>
801 --           <li><i>serverinfo:</i> arbitrary information about the above
802 --                                  server</li>
803 --           <li><i>awaymsg:</i>    set to the user's away message if they are
804 --                                  away</li>
805 --           <li><i>is_oper:</i>    true if the user is an IRCop</li>
806 --           <li><i>idle_time:</i>  amount of time the user has been idle</li>
807 --           <li><i>channels:</i>   array containing the channels the user has
808 --                                  joined</li>
809 --           </ul>
810 -- @param nick User to request WHOIS information about
811 function whois(cb, nick)
812     nick = nick:lower()
813     requestinfo.whois[nick] = {}
814     if not icallbacks.whois[nick] then
815         icallbacks.whois[nick] = {cb}
816         send("WHOIS", nick)
817     else
818         table.insert(icallbacks.whois[nick], cb)
819     end
820 end
821 -- }}}
822
823 -- server_time {{{
824 ---
825 -- Request the current time of the server you are connected to.
826 -- @param cb Callback to call when the information is available. The single
827 --           table parameter to this callback will contain the fields:
828 --           <ul>
829 --           <li><i>server:</i> the server which responded to the request</li>
830 --           <li><i>time:</i>   the time reported by the server</li>
831 --           </ul>
832 function server_time(cb)
833     -- apparently the optional server parameter isn't supported for servers
834     -- which you are not directly connected to (freenode specific?)
835     local server = serverinfo.host
836     if not icallbacks.servertime[server] then
837         icallbacks.servertime[server] = {cb}
838         send("TIME", server)
839     else
840         table.insert(icallbacks.servertime[server], cb)
841     end
842 end
843 -- }}}
844 -- }}}
845
846 -- ctcp commands {{{
847 -- ctcp_ping {{{
848 ---
849 -- Send a CTCP ping request.
850 -- @param cb Callback to call when the information is available. The single
851 --           table parameter to this callback will contain the fields:
852 --           <ul>
853 --           <li><i>nick:</i> the nick which responded to the request</li>
854 --           <li><i>time:</i> the roundtrip ping time, in seconds</li>
855 --           </ul>
856 -- @param nick User to ping
857 function ctcp_ping(cb, nick)
858     nick = nick:lower()
859     if not icallbacks.ctcp_ping[nick] then
860         icallbacks.ctcp_ping[nick] = {cb}
861         say(nick, c("PING", os.time()))
862     else
863         table.insert(icallbacks.ctcp_ping[nick], cb)
864     end
865 end
866 -- }}}
867
868 -- ctcp_time {{{
869 ---
870 -- Send a localtime request.
871 -- @param cb Callback to call when the information is available. The single
872 --           table parameter to this callback will contain the fields:
873 --           <ul>
874 --           <li><i>nick:</i> the nick which responded to the request</li>
875 --           <li><i>time:</i> the localtime reported by the remote client</li>
876 --           </ul>
877 -- @param nick User to request the localtime from
878 function ctcp_time(cb, nick)
879     nick = nick:lower()
880     if not icallbacks.ctcp_time[nick] then
881         icallbacks.ctcp_time[nick] = {cb}
882         say(nick, c("TIME"))
883     else
884         table.insert(icallbacks.ctcp_time[nick], cb)
885     end
886 end
887 -- }}}
888
889 -- ctcp_version {{{
890 ---
891 -- Send a client version request.
892 -- @param cb Callback to call when the information is available. The single
893 --           table parameter to this callback will contain the fields:
894 --           <ul>
895 --           <li><i>nick:</i>    the nick which responded to the request</li>
896 --           <li><i>version:</i> the version reported by the remote client</li>
897 --           </ul>
898 -- @param nick User to request the client version from
899 function ctcp_version(cb, nick)
900     nick = nick:lower()
901     if not icallbacks.ctcp_version[nick] then
902         icallbacks.ctcp_version[nick] = {cb}
903         say(nick, c("VERSION"))
904     else
905         table.insert(icallbacks.ctcp_version[nick], cb)
906     end
907 end
908 -- }}}
909 -- }}}
910
911 -- callback functions {{{
912 -- register_callback {{{
913 ---
914 -- Register a user function to be called when a specific event occurs.
915 -- @param name Name of the event
916 -- @param fn   Function to call when the event occurs, or nil to clear the
917 --             callback for this event
918 -- @return Value of the original callback for this event (or nil if no previous
919 --         callback had been set)
920 function register_callback(name, fn)
921     local old_handler = user_handlers[name]
922     user_handlers[name] = fn
923     return old_handler
924 end
925 -- }}}
926 -- }}}
927
928 -- misc functions {{{
929 -- send {{{
930 -- TODO: CTCP quoting should be explicit, this table thing is quite ugly (if
931 -- convenient)
932 ---
933 -- Send a raw IRC command.
934 -- @param command String containing the raw IRC command
935 -- @param ...     Arguments to the command. Each argument is either a string or
936 --                an array. Strings are sent literally, arrays are CTCP quoted
937 --                as a group. The last argument (if it exists) is preceded by
938 --                a : (so it may contain spaces).
939 function send(command, ...)
940     if not serverinfo.connected and not serverinfo.connecting then return end
941     local message = command
942     for i, v in base.ipairs({...}) do
943         if i == #{...} then
944             v = ":" .. v
945         end
946         message = message .. " " .. v
947     end
948     message = ctcp._low_quote(message)
949     -- we just truncate for now. -2 to account for the \r\n
950     message = message:sub(1, constants.IRC_MAX_MSG - 2)
951     irc_debug._message("SEND", message)
952     irc_sock:send(message .. "\r\n")
953 end
954 -- }}}
955
956 -- get_ip {{{
957 ---
958 -- Get the local IP address for the server connection.
959 -- @return A string representation of the local IP address that the IRC server
960 --         connection is communicating on
961 function get_ip()
962     return (ip or irc_sock:getsockname())
963 end
964 -- }}}
965
966 -- set_ip {{{
967 ---
968 -- Set the local IP manually (to allow for NAT workarounds)
969 -- @param new_ip IP address to set
970 function set_ip(new_ip)
971     ip = new_ip
972 end
973 -- }}}
974
975 -- channels {{{
976 -- TODO: @see doesn't currently work for files/modules
977 ---
978 -- Iterate over currently joined channels.
979 -- channels() is an iterator function for use in for loops.
980 -- For example, <pre>for chan in irc.channels() do print(chan:name) end</pre>
981 -- @see irc.channel
982 function channels()
983     return function(state, arg)
984                return misc._value_iter(state, arg,
985                                        function(v)
986                                            return v.join_complete
987                                        end)
988            end,
989            serverinfo.channels,
990            nil
991 end
992 -- }}}
993 -- }}}
994 -- }}}