Preliminary RSS plug-in
[six.git] / lib / irc.rb
blobf347c5938d146f6fb5d4230a626c42083b0d1e24
3 # CyBot - IRC module.
6 require 'ircbase/irc'
8 # Class to interact with IRC from plugins.
9 class IrcWrapper
11   # Private.
12   def initialize(from, server, channel = nil)
13     @from = from
14     @server = server
15     @channel = channel
16   end
18   # Direct reply.
19   def privmsg_reply(str)
20     @channel ? @channel.privmsg("#{@from.nick}: #{str}") : @from.privmsg(str)
21   end
22   def notice_reply(str)
23     @channel ? @channel.notice("#{@from.nick}: #{str}") : @from.notice(str)
24   end
26   # Direct response.
27   def privmsg_respond(str)
28     @channel ? @channel.privmsg(str) : @from.privmsg(str)
29   end
30   def notice_respond(str)
31     @channel ? @channel.notice(str) : @from.notice(str)
32   end
34   # Reply through default means.
35   def reply(str)
36     $config['comm/use-notices'] ? notice_reply(str) : privmsg_reply(str)
37   end
38   def respond(str)
39     $config['comm/use-notices'] ? notice_respond(str) : privmsg_respond(str)
40   end
42   # Direct output.
43   def privmsg(str)
44     @channel ? @channel.privmsg(str) : @from.privmsg(str)
45   end
46   def notice(str)
47     @channel ? @channel.notice(str) : @from.notice(str)
48   end
49   def puts(str)
50     $config['comm/use-notices'] ? notice(str) : privmsg(str)
51   end
52   alias_method :say, :puts
53   def action(str)
54     @channel ? @channel.action(str) : @from.action(str)
55   end
57   # Access to the rest.
58   attr_accessor :from, :channel
59   attr_reader :server
61 end
64 # Class name must match file name, capitalized if needed.
65 # A single instance of the class will be created, for now.
66 # Later, options for per-network or per-channel.
68 class Irc < PluginBase
70   # Fetch a command for execution. Reports errors to the user, and
71   # returns nil or [cmd, name, line] if successful.
72   def get_command(irc, line)
73     cmd_name, line = line.split(' ', 2)
74     if cmd_name == '?'
75       irc.reply "Type '<plugin name>?' to get brief help and a command list, where <plugin name> is one of: \x02#{$plugins.keys.sort.join(', ')}.\x0f  Also try '<plugin name> <command name>?'."
76       return nil
77                 elsif cmd_name == "help" || cmd_name == "help?" # Don't name a plugin help!
78                         irc.reply "Try '?' instead of help."
79     end
80     (help = cmd_name[-1] == ??) and cmd_name.chop!
81     cmd = $commands[cmd_name]
82     if !cmd or cmd[0] == Plugin::CmdFlag_Multiple
83       plugin_name = cmd_name
84       unless (plugin = $plugins[plugin_name])
85         irc.reply !cmd ? "Unknown command or plugin '#{plugin_name}'." :
86           "Command name is ambiguous. Please use the syntax '<plugin> #{cmd_name}' instead, where <plugin> is one of: \x02#{cmd[1..-1].map { |c| c[2].name }.sort.join(', ')}.\x0f"
87         return nil
88       end
89       if help or line == '?'
90         irc.reply "#{plugin.brief_help}  Commands: \x02#{plugin.commands.keys.sort.join(', ')}.\x0f"
91         return nil
92       end
93       unless line
94         irc.reply "Type '#{plugin_name} <command>', where command is one of: \x02#{plugin.commands.keys.sort.join(', ')}.\x0f  Also try '#{plugin_name}?'."
95         return nil
96       end
97       cmd_name, line = line.split(' ', 2)
98       (help = cmd_name[-1] == ??) and cmd_name.chop!
99       unless (cmd = plugin.commands[cmd_name])
100         irc.reply "No '#{cmd_name}' command in the '#{plugin_name}' plugin.  Try '#{plugin_name}?'."
101         return nil
102       end
103     end
104     if help
105       irc.reply cmd[3] || 'No help for this command, sorry :-('
106       return nil
107     end
108     [cmd, cmd_name, line]
109   end
111   class Channel < IRC::Channel
113     attr_reader :plugins
115     def is_alpha(c)
116       c and (c >= ?A && c <= ?Z or c >= ?a && c <= ?z)
117     end
119     def on_privmsg(from, message)
121       # If it's for us, and checks out.
122       if message[0] == ?$ and is_alpha(message[1]) || message[1] == ??
123         irc = IrcWrapper.new(from, @server, self)
124         cmd, cmd_name, line = $irc.get_command(irc, message[1..-1])
125         if cmd
126           flags, meth, ins = cmd
127           begin
129             # Security check.
130             $user.command_check(irc, ins.name, cmd_name)
131             $cmd_cnt += 1
133             # Bind and execute!
134             line.strip! if line
135             if flags & Plugin::CmdFlag_Server != 0
136               meth.call(irc, @server, line)
137             elsif flags & Plugin::CmdFlag_Channel != 0
138               meth.call(irc, self, line)
139             else
140               meth.call(irc, line)
141             end
142           rescue Plugin::ParseError => e
143             irc.reply e.message
144             irc.reply "USAGE: #{cmd_name} #{e.usage}."
145           rescue User::SecurityError => e
146             irc.reply e.message
147           rescue Exception => e
148             $log.puts e.message
149           end
151         end
152       end
153       call_hooks(:privmsg_chan, from, message)
154     end
156     def on_notice(from, message)
158       if message[0] == ?!
159         $log.puts "-- [#{@name}] #{from}: #{message[1..-1]}"
160       end
162       # Invoke hooks.
163       call_hooks(:notice_chan, from, message)
165     end
167     def on_join(user)
168       call_hooks(:join_chan, user)
169     end
171     def on_topic(user)
172       call_hooks(:topic_chan, user, @topic)
173     end
175     # Called when channel join is done.
176     def on_init
178       # Request modes from ChanServ, if needed.
179       $log.puts "Channel #{@name} joined!"
180       if (cs = $config["servers/#{@server.name}/channels/#{@nname}/chanserv"])
181         name = $config["servers/#{@server.name}/services/chanserv/name"] || 'ChanServ'
182         $log.puts "Requesting modes from #{name}..."
183         @server.cmd('PRIVMSG', name, "OP #{@name}") if cs['op']
184         @server.cmd('PRIVMSG', name, "VOICE #{@name}") if cs['voice']
185       end
187       # Call plugin hooks.
188       call_hooks(:init_chan)
190     end
192     def on_part(user)
193       call_hooks(:part_chan, user)
194     end
196     def on_command(handled, from, cmd, *data)
197       call_hooks(:command_chan, from, handled, cmd, *data)
198     end
200     def on_reply(code, *data)
201       call_hooks(:reply_chan, nil, code, *data)
202     end
204     def call_hooks(hook, from = nil, *msg)
205       if (h = $hooks[hook])
206         irc = nil
207         h.each do |i|
208           irc ||= IrcWrapper.new(from, @server, self)
209           begin
210             i[0].call(irc, *msg)
211           rescue Exception => e
212             $log.puts e.message
213             $log.puts e.backtrace.join("\n")
214           end
215         end
216       end
217     end
219 #    def reply_hook(code, *data)
220 #      super(code, *data)
221 #  #    $log.puts "[#{@name}] (#{code}) <#{data.length}> #{data.inspect}"
222 #    end
224   end
226   class Server < IRC::Server
228     def initialize(name, *args)
229       super(*args)
230       @name = name
231     end
233     attr_reader :plugins, :name
235     def channel_class
236       Channel
237     end
239     def call_hooks(hook, from, *msg)
240       if (h = $hooks[hook])
241         irc = nil
242         h.each do |i|
243           irc ||= IrcWrapper.new(from, self)
244           begin
245             i[0].call(irc, *msg)
246           rescue Exception => e
247             $log.puts e.message
248             $log.puts e.backtrace.join("\n")
249           end
250         end
251       end
252     end
254     # Called when we're connected.
255     # Register with NickServ, if required.
256     def on_connect
257       $log.puts "Connection to '#{@name}' completed."
258       if (ns = $config["servers/#{@name}/services/nickserv"]) and (pw = ns['password'])
259         name = ns['name'] || 'NickServ'
260         $log.puts "Registering with #{name}..."
261         cmd('PRIVMSG', name, 'IDENTIFY', pw)
262         cmd('PRIVMSG', name, "IDENTIFY #{pw}")
263       end
264     end
266     def on_privmsg(from, message)
267       $log.puts "#{from} told us: #{message.inspect}"
269       # Execute command, if authorized. (remove)
270       # if (from.nick == 'cryo' or from.nick == 'cyanite') and message[0] == ?&
271       #   begin
272       #     eval message[1..-1]
273       #   rescue Exception => e
274       #     $log.puts "Error evaluating code: #{e.message}"
275       #   end
276       # end
278       # Look for command.
279       irc = IrcWrapper.new(from, self)
280       cmd, cmd_name, line = $irc.get_command(irc, message)
281       if cmd
282         flags, meth, ins = cmd
283         begin
285           # Security check.
286           $user.command_check(irc, ins.name, cmd_name)
287           $cmd_cnt += 1
289           # Server-bound command...
290           if flags & Plugin::CmdFlag_Server != 0
292             # Grab _optional_ server argument.
293             # FIXME: Security check.
294             if line
295               line.strip!
296               if line[0] == ?@
297                 sn, line = line[1..-1].split(' ', 2)
298                 line.strip! if line
299               end
300             end
302             # Execute.
303             if sn
304               if (s = $irc.servers[sn])
305                 meth.call(irc, s, line)
306               else irc.reply "Error: The bot isn't connected to the server '#{sn}'."
307               end
308             else meth.call(irc, self, line)
309             end
311           # Channel-bound command...
312           elsif flags & Plugin::CmdFlag_Channel != 0
314             # Grab server/channel arguments.
315             if line
316               t, line = line.split(' ', 2)
317               if t and t[0] == ?@
318                 sn = t[1..-1]
319                 t, line = line.split(' ', 2)
320               else sn = nil end
321               cn = (t and '#&+!'.include?(t[0])) ? t : nil
322             else cn = nil end
324             # Check if valid. Execute or report error.
325             if cn
326               if (s = sn ? $irc.servers[sn] : self)
327                 if (c = s.channels[cn.downcase])
328                   line.strip! if line
329                   meth.call(irc, c, line)
330                 else irc.reply "Error: The bot isn't on the channel '#{cn}'."
331                 end
332               else irc.reply "Error: The bot isn't connected to the server '#{sn}'."
333               end
334             else irc.reply "Error: When invoked in private, this command requires a bot-channel name."
335             end
337           # Regular (unbound) command...
338           else
339             line.strip! if line
340             meth.call(irc, line)
341           end
343         rescue Plugin::ParseError => e
344           irc.reply e.message
345           irc.reply "USAGE: #{cmd_name} #{e.usage}."
346         rescue User::SecurityError => e
347           irc.reply e.message
348         rescue Exception => e
349           $log.puts e.message
350         end
352       end
353       call_hooks(:privmsg_priv, from, message)
354     end
356     def on_notice(from, message)
357       $log.puts "#{from} noted: #{message}"
358       call_hooks(:notice_priv, from, message)
359     end
361     def on_command(handled, from, cmd, *data)
362       call_hooks(:command_serv, from, handled, cmd, *data)
363     end
365     def on_reply(code, *data)
366       call_hooks(:reply_serv, nil, code, *data)
367     end
369   end
371   def initialize(*args)
373     @brief_help = 'Manages IRC connectivity.'
374     super(*args)
375     @servers = {}
376     @start = Time.now
377     $irc = self
378     $cmd_cnt = 0
380     # Common map spaces.
381     chan_common = {
382       :help => 'Channels this bot should be on, in addition to the global list (irc/channels).',
383       :dir => true,
384       :skel => {
385         :help => 'Channel settings.',
386         :dir => true,
387         # FIXME: Move to User plugin.
388         'enforce' => {
389           :help => 'Enforce user capabilities on this channel, changing modes as necessary.',
390           :type => Boolean
391         },
392         'promote' => {
393           :help => 'Positively enforce user capabilities on this channel.',
394           :type => Boolean
395         },
396         'demote' => {
397           :help => 'Negatively enforce user capabilities on this channel.',
398           :type => Boolean
399         },
400         'user-greeting' => 'Greeting text for users joining the channel.',
401         'chanserv' => {
402           :help => 'Settings for ChanServ.',
403           :dir => true,
404           'op' => {
405             :help => 'Request and attempt to keep OP on this channel.',
406             :type => Boolean
407           },
408           'voice' => {
409             :help => 'Request and attempt to keep VOICE on this channel.',
410             :type => Boolean
411           }
412         }
413       }
414     }
416     # Set-up config space.
417     $config.merge(
418       :help => 'CyBot configuration directory.',
419       'comm' => {
420         :dir => true,
421         :help => 'Bot communication settings.',
422         'use-notices' => {
423           :help => 'Use notices instead of messages for channel replies.',
424           :type => Boolean
425         }
426       },
427       'plugins' => {
428         :dir => true,
429         :help => 'Plugin configuration directoy.'
430       },
431       'irc' => {
432         :dir => true,
433         :help => 'Global IRC settings.',
434         'channels' => chan_common,
435         'nicks' => {
436           :help => 'Nick names to use.',
437           :type => Array
438         }
439       },
440       'servers' => {
441         :help  => 'List of IRC servers to connect to.',
442         :dir => true,
443         :skel => {
444           :dir => true,
445           :help => 'Settings for the IRC server.',
446           'host' => 'Host of the IRC server.',
447           'port' => 'TCP port of the IRC server.',
448           'nicks' => {
449             :help => 'Nick names to use. Replaces global list (irc/nicks).',
450             :type => Array
451           },
452           'services' => {
453             :help => 'Settings for IRC services.',
454             :dir => true,
455             'nickserv' => {
456               :help => 'Settings for identifying with NickServ.',
457               :dir => true,
458               'name' => 'Nick name of NickServ. Defaults to NickServ.',
459               'password' => 'Password used to identify with NickServ.'
460             },
461             'chanserv' => {
462               :help => 'Settings for ChanServ.',
463               :dir => true,
464               'name' => 'Nick name of ChanServ. Defaults to ChanServ.'
465             }
466           },
467           'channels' => chan_common,
468           'user-greeting' => 'Greeting text for users joining a channel.',
469         }
470       }
471     )
473     # Make sure we have a default list of nick names.
474     nicks = $config['irc/nicks', ['CyBot', '_cybot_', '__cybot']]
475     chans = []
476     if (chan_dir = $config['irc/channels'])
477       chan_dir.each { |k,v| chans << k unless v['autojoin'] == false }
478     end
480     # Connect to servers in the list.
481     if (servs = $config['servers'])
482       servs.each do |k,v|
484         # Figure out parameters.
485         next if !(ac = v['autoconnect']).nil? and ac == false
486         host = v['host']  || k
487         port = v['port']  || 6667
488         nick = v['nicks'] || nicks
489         chan = chans.dup
490         if (chan_dir = v['channels'])
491           chan_dir.each { |ck,cv| chan << ck unless cv['autojoin'] == false }
492         end
494         # Connect!
495         @servers[k] = Server.new(k, host, nick,
496           :port => port,
497           :user => 'CyBot',
498           :channels => chan
499         )
501       end
502     end
504   end
506   attr_reader :servers
508   # We provide some commands as well.
510   # Uptime and status.
511   def cmd_uptime(irc, line)
512     up_for = (Time.now - @start).to_i
513     irc.reply "I have been running for #{seconds_to_s(up_for, irc)} during which I processed #{$cmd_cnt} commands."
514   end
515   help :uptime, 'Displays the current bot uptime.'
517   def cmd_version(irc, line)
518     irc.action "is an embodiment of CyBot v#{$version}."
519   end
520   help :version, 'Displays the CyBot release number.'
522   def serv_join(irc, serv, line)
523     if !$user.caps(irc, 'admin', 'owner').any?
524       irc.reply "You need the 'admin' capability for this."
525     elsif !line or line.empty?
526       irc.reply 'USAGE: join [-]<channel name> [s]'
527     else
528       chan, sticky = line.split(' ', 2)
529       force = false
530       if chan[0] == ?-
531         chan = chan[1..-1]
532         force = true
533       end
534       chan = '#' + chan unless chan[0] == ?#
535       if serv.join(chan, force)
536         if sticky and sticky == 's'
537           $config["servers/#{serv.name}/channels/#{chan}"] = {}
538         end
539       elsif !force
540         irc.reply "I seem to be already joined to that channel. If you really mean it, use join -<channel name> to make me try to join it anyway."
541       end
542     end
543   end
544   help :join, "Makes the bot join a channel. Supply the 's' option to make the join sticky."
546   def chan_part(irc, chan, line)
547     if !$user.caps(irc, 'admin', 'owner').any?
548       irc.reply "You need the 'admin' capability for this."
549     else
550       line = nil if line and line.empty?
551       chan.part(line)
552     end
553   end
554   help :part, 'Makes the bot leave a channel, with an optional reason.'