8 # Class to interact with IRC from plugins.
12 def initialize(from, server, channel = nil)
19 def privmsg_reply(str)
20 @channel ? @channel.privmsg("#{@from.nick}: #{str}") : @from.privmsg(str)
23 @channel ? @channel.notice("#{@from.nick}: #{str}") : @from.notice(str)
27 def privmsg_respond(str)
28 @channel ? @channel.privmsg(str) : @from.privmsg(str)
30 def notice_respond(str)
31 @channel ? @channel.notice(str) : @from.notice(str)
34 # Reply through default means.
36 $config['comm/use-notices'] ? notice_reply(str) : privmsg_reply(str)
39 $config['comm/use-notices'] ? notice_respond(str) : privmsg_respond(str)
44 @channel ? @channel.privmsg(str) : @from.privmsg(str)
47 @channel ? @channel.notice(str) : @from.notice(str)
50 $config['comm/use-notices'] ? notice(str) : privmsg(str)
52 alias_method :say, :puts
54 @channel ? @channel.action(str) : @from.action(str)
58 attr_accessor :from, :channel
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)
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>?'."
77 elsif cmd_name == "help" || cmd_name == "help?" # Don't name a plugin help!
78 irc.reply "Try '?' instead of help."
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"
89 if help or line == '?'
90 irc.reply "#{plugin.brief_help} Commands: \x02#{plugin.commands.keys.sort.join(', ')}.\x0f"
94 irc.reply "Type '#{plugin_name} <command>', where command is one of: \x02#{plugin.commands.keys.sort.join(', ')}.\x0f Also try '#{plugin_name}?'."
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}?'."
105 irc.reply cmd[3] || 'No help for this command, sorry :-('
108 [cmd, cmd_name, line]
111 class Channel < IRC::Channel
116 c and (c >= ?A && c <= ?Z or c >= ?a && c <= ?z)
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])
126 flags, meth, ins = cmd
130 $user.command_check(irc, ins.name, cmd_name)
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)
142 rescue Plugin::ParseError => e
144 irc.reply "USAGE: #{cmd_name} #{e.usage}."
145 rescue User::SecurityError => e
147 rescue Exception => e
153 call_hooks(:privmsg_chan, from, message)
156 def on_notice(from, message)
159 $log.puts "-- [#{@name}] #{from}: #{message[1..-1]}"
163 call_hooks(:notice_chan, from, message)
168 call_hooks(:join_chan, user)
172 call_hooks(:topic_chan, user, @topic)
175 # Called when channel join is done.
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']
188 call_hooks(:init_chan)
193 call_hooks(:part_chan, user)
196 def on_command(handled, from, cmd, *data)
197 call_hooks(:command_chan, from, handled, cmd, *data)
200 def on_reply(code, *data)
201 call_hooks(:reply_chan, nil, code, *data)
204 def call_hooks(hook, from = nil, *msg)
205 if (h = $hooks[hook])
208 irc ||= IrcWrapper.new(from, @server, self)
211 rescue Exception => e
213 $log.puts e.backtrace.join("\n")
219 # def reply_hook(code, *data)
221 # # $log.puts "[#{@name}] (#{code}) <#{data.length}> #{data.inspect}"
226 class Server < IRC::Server
228 def initialize(name, *args)
233 attr_reader :plugins, :name
239 def call_hooks(hook, from, *msg)
240 if (h = $hooks[hook])
243 irc ||= IrcWrapper.new(from, self)
246 rescue Exception => e
248 $log.puts e.backtrace.join("\n")
254 # Called when we're connected.
255 # Register with NickServ, if required.
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}")
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] == ?&
272 # eval message[1..-1]
273 # rescue Exception => e
274 # $log.puts "Error evaluating code: #{e.message}"
279 irc = IrcWrapper.new(from, self)
280 cmd, cmd_name, line = $irc.get_command(irc, message)
282 flags, meth, ins = cmd
286 $user.command_check(irc, ins.name, cmd_name)
289 # Server-bound command...
290 if flags & Plugin::CmdFlag_Server != 0
292 # Grab _optional_ server argument.
293 # FIXME: Security check.
297 sn, line = line[1..-1].split(' ', 2)
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}'."
308 else meth.call(irc, self, line)
311 # Channel-bound command...
312 elsif flags & Plugin::CmdFlag_Channel != 0
314 # Grab server/channel arguments.
316 t, line = line.split(' ', 2)
319 t, line = line.split(' ', 2)
321 cn = (t and '#&+!'.include?(t[0])) ? t : nil
324 # Check if valid. Execute or report error.
326 if (s = sn ? $irc.servers[sn] : self)
327 if (c = s.channels[cn.downcase])
329 meth.call(irc, c, line)
330 else irc.reply "Error: The bot isn't on the channel '#{cn}'."
332 else irc.reply "Error: The bot isn't connected to the server '#{sn}'."
334 else irc.reply "Error: When invoked in private, this command requires a bot-channel name."
337 # Regular (unbound) command...
343 rescue Plugin::ParseError => e
345 irc.reply "USAGE: #{cmd_name} #{e.usage}."
346 rescue User::SecurityError => e
348 rescue Exception => e
353 call_hooks(:privmsg_priv, from, message)
356 def on_notice(from, message)
357 $log.puts "#{from} noted: #{message}"
358 call_hooks(:notice_priv, from, message)
361 def on_command(handled, from, cmd, *data)
362 call_hooks(:command_serv, from, handled, cmd, *data)
365 def on_reply(code, *data)
366 call_hooks(:reply_serv, nil, code, *data)
371 def initialize(*args)
373 @brief_help = 'Manages IRC connectivity.'
382 :help => 'Channels this bot should be on, in addition to the global list (irc/channels).',
385 :help => 'Channel settings.',
387 # FIXME: Move to User plugin.
389 :help => 'Enforce user capabilities on this channel, changing modes as necessary.',
393 :help => 'Positively enforce user capabilities on this channel.',
397 :help => 'Negatively enforce user capabilities on this channel.',
400 'user-greeting' => 'Greeting text for users joining the channel.',
402 :help => 'Settings for ChanServ.',
405 :help => 'Request and attempt to keep OP on this channel.',
409 :help => 'Request and attempt to keep VOICE on this channel.',
416 # Set-up config space.
418 :help => 'CyBot configuration directory.',
421 :help => 'Bot communication settings.',
423 :help => 'Use notices instead of messages for channel replies.',
429 :help => 'Plugin configuration directoy.'
433 :help => 'Global IRC settings.',
434 'channels' => chan_common,
436 :help => 'Nick names to use.',
441 :help => 'List of IRC servers to connect to.',
445 :help => 'Settings for the IRC server.',
446 'host' => 'Host of the IRC server.',
447 'port' => 'TCP port of the IRC server.',
449 :help => 'Nick names to use. Replaces global list (irc/nicks).',
453 :help => 'Settings for IRC services.',
456 :help => 'Settings for identifying with NickServ.',
458 'name' => 'Nick name of NickServ. Defaults to NickServ.',
459 'password' => 'Password used to identify with NickServ.'
462 :help => 'Settings for ChanServ.',
464 'name' => 'Nick name of ChanServ. Defaults to ChanServ.'
467 'channels' => chan_common,
468 'user-greeting' => 'Greeting text for users joining a channel.',
473 # Make sure we have a default list of nick names.
474 nicks = $config['irc/nicks', ['CyBot', '_cybot_', '__cybot']]
476 if (chan_dir = $config['irc/channels'])
477 chan_dir.each { |k,v| chans << k unless v['autojoin'] == false }
480 # Connect to servers in the list.
481 if (servs = $config['servers'])
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
490 if (chan_dir = v['channels'])
491 chan_dir.each { |ck,cv| chan << ck unless cv['autojoin'] == false }
495 @servers[k] = Server.new(k, host, nick,
508 # We provide some commands as well.
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."
515 help :uptime, 'Displays the current bot uptime.'
517 def cmd_version(irc, line)
518 irc.action "is an embodiment of CyBot v#{$version}."
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]'
528 chan, sticky = line.split(' ', 2)
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}"] = {}
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."
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."
550 line = nil if line and line.empty?
554 help :part, 'Makes the bot leave a channel, with an optional reason.'