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