lib/robut/plugin.rb in robut-0.3.0 vs lib/robut/plugin.rb in robut-0.4.0
- old
+ new
@@ -1,10 +1,49 @@
# Robut plugins implement a simple interface to listen for messages
# and optionally respond to them. All plugins include the Robut::Plugin
# module.
module Robut::Plugin
+ # Contains methods that will be added directly to a class including
+ # Robut::Plugin.
+ module ClassMethods
+
+ # Sets up a 'matcher' that will try to match input being sent to
+ # this plugin with a regular expression. If the expression
+ # matches, +action+ will be performed. +action+ will be passed any
+ # captured groups in the regular expression as parameters. For
+ # example:
+ #
+ # match /^say hello to (\w+)/ do |name| ...
+ #
+ # The action is run in the context of an instance of a class that
+ # includes Robut::Plugin. Like +handle+, an action explicitly
+ # returning +true+ will stop the plugin chain from matching any
+ # further.
+ #
+ # Supported options:
+ # :sent_to_me - only try to match this regexp if it contains an @reply to robut.
+ # This will also strip the @reply from the message we're trying
+ # to match on, so ^ and $ will still do the right thing.
+ def match(regexp, options = {}, &action)
+ matchers << [regexp, options, action, @last_description]
+ @last_description = nil
+ end
+
+ # Provides a description for the next matcher
+ def desc(string)
+ @last_description = string
+ end
+
+ # A list of regular expressions to apply to input being sent to
+ # the plugin, along with blocks of actions to perform. Each
+ # element is a [regexp, options, action, description] array.
+ def matchers
+ @matchers ||= []
+ end
+ end
+
class << self
# A list of all available plugin classes. When you require a new
# plugin class, you should add it to this list if you want it to
# respond to messages.
attr_accessor :plugins
@@ -19,57 +58,65 @@
# If we are handling a private message, holds a reference to the
# sender of the message. +nil+ if the message was sent to the entire
# room.
attr_accessor :private_sender
- # Creates a new instance of this plugin that references the
- # specified connection.
- def initialize(connection, private_sender = nil)
- self.connection = connection
+ attr_accessor :reply_to
+
+ # :nodoc:
+ def self.included(klass)
+ klass.send(:extend, ClassMethods)
+ end
+
+ # Creates a new instance of this plugin to reply to a particular
+ # object over that object's connection
+ def initialize(reply_to, private_sender = nil)
+ self.reply_to = reply_to
+ self.connection = reply_to.connection
self.private_sender = private_sender
end
# Send +message+ back to the HipChat server. If +to+ == +:room+,
# replies to the room. If +to+ == nil, responds in the manner the
# original message was sent. Otherwise, PMs the message to +to+.
def reply(message, to = nil)
if to == :room
- connection.reply(message, nil)
+ reply_to.reply(message, nil)
else
- connection.reply(message, to || private_sender)
+ reply_to.reply(message, to || private_sender)
end
end
# An ordered list of all words in the message with any reference to
# the bot's nick stripped out. If +command+ is passed in, it is also
# stripped out. This is useful to separate the 'parameters' from the
# 'commands' in a message.
def words(message, command = nil)
- reply = at_nick
+ reply = at_nick.downcase
command = command.downcase if command
message.split.reject {|word| word.downcase == reply || word.downcase == command }
end
# Removes the first word in message if it is a reference to the bot's nick
# Given "@robut do this thing", Returns "do this thing"
def without_nick(message)
possible_nick, command = message.split(' ', 2)
- if possible_nick == at_nick
+ if possible_nick.casecmp(at_nick) == 0
command
else
message
end
end
# The bot's nickname, for @-replies.
def nick
- connection.config.nick.split.first
+ connection.config.mention_name || connection.config.nick.gsub(/[^0-9a-z]/i, '')
end
# #nick with the @-symbol prepended
def at_nick
- "@#{nick.downcase}"
+ "@#{nick}"
end
# Was +message+ sent to Robut as an @reply?
def sent_to_me?(message)
message =~ /(^|\s)@#{nick}(\s|$)/i
@@ -78,24 +125,58 @@
# Do whatever you need to do to handle this message.
# If you want to stop the plugin execution chain, return +true+ from this
# method. Plugins are handled in the order that they appear in
# Robut::Plugin.plugins
def handle(time, sender_nick, message)
- raise NotImplementedError, "Implement me in #{self.class.name}!"
+ if matchers.empty?
+ raise NotImplementedError, "Implement me in #{self.class.name}!"
+ else
+ find_match(message)
+ end
end
# Returns a list of messages describing the commands this plugin
# handles.
def usage
+ matchers.map do |regexp, options, action, description|
+ next unless description
+ if options[:sent_to_me]
+ at_nick + " " + description
+ else
+ description
+ end
+ end.compact
end
def fake_message(time, sender_nick, msg)
# TODO: ensure this connection is threadsafe
- plugins = Robut::Plugin.plugins.map { |p| p.new(connection, private_sender) }
- connection.handle_message(plugins, time, sender_nick, msg)
+ plugins = Robut::Plugin.plugins.map { |p| p.new(reply_to, private_sender) }
+ reply_to.handle_message(plugins, time, sender_nick, msg)
end
# Accessor for the store instance
def store
connection.store
+ end
+
+ private
+
+ # Find and run all the actions associated with matchers that match
+ # +message+.
+ def find_match(message)
+ matchers.each do |regexp, options, action, description|
+ if options[:sent_to_me] && !sent_to_me?(message)
+ next
+ end
+
+ if match_data = without_nick(message).match(regexp)
+ # Return true explicitly if this matcher explicitly returned true
+ break true if instance_exec(*match_data[1..-1], &action) == true
+ end
+ end
+ end
+
+ # The matchers defined by this plugin's class
+ def matchers
+ self.class.matchers
end
end