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