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