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