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