# exisitng rbot command and that is run.
#
# == Example Session
-# < alias googlerbot *terms => google site:linuxbrit.co.uk/rbot/ <terms>
+# < alias googlerbot *terms => google site:ruby-rbot.org <terms>
# > okay
# < googlerbot plugins
-# > Results for site:linuxbrit.co.uk/rbot/ plugins: ....
+# > Results for site:ruby-rbot.org plugins: ....
#
# == Security
# By default, only the owner can define and remove aliases, while everyone else can
# to detect and stop this, but a few recursive calls can still cause spamming
require 'yaml'
+require 'set'
class AliasPlugin < Plugin
# an exception raised when loading or getting input of invalid alias definitions
class AliasDefinitionError < ArgumentError
end
- MAX_RECURSION_DEPTH = 10
-
def initialize
super
- @data_path = "#{@bot.botclass}/alias/"
- @data_file = "#{@data_path}/aliases.yaml"
+ @data_path = datafile
+ @data_file = File.join(@data_path, 'aliases.yaml')
# hash of alias => command entries
- @aliases = if File.exist?(@data_file)
- YAML.load_file(@data_file)
- else
- Hash.new
+ data = nil
+ aliases = if File.exist?(@data_file) &&
+ (data = YAML.load_file(@data_file)) &&
+ data.respond_to?(:each_pair)
+ data
+ else
+ warning _("Data file is not found or corrupt, reinitializing data")
+ Hash.new
end
- @aliases.each_pair do |a, c|
+ @aliases = Hash.new
+ aliases.each_pair do |a, c|
begin
add_alias(a, c)
rescue AliasDefinitionError
warning _("Invalid alias entry %{alias} : %{command} in %{filename}: %{reason}") %
{:alias => a, :command => c, :filename => @data_file, :reason => $1}
end
- end
- end
+ end
+ end
- def save
- Dir.mkdir(@data_path) unless File.exist?(@data_path)
- File.open(@data_file, 'w') {|f| f.write @aliases.to_yaml}
+ def save
+ FileUtils.mkdir_p(@data_path)
+ Utils.safe_save(@data_file) {|f| f.write @aliases.to_yaml}
end
def cmd_add(m, params)
# each alias is implemented by adding a message map, whose handler creates a message
# containing the aliased command
- command.grep(/<(\w+)>/) {$1}.all? {|s| text =~ /(?:^|\s)[:*]#{s}(?:\s|$)/ } or
- raise AliasDefinitionError.new(_('Not all substitutions in command text have matching arguments in alias text'))
-
+ command.scan(/<(\w+)>/).flatten.to_set ==
+ text.split.grep(/\A[:*](\w+)\Z/) {$1}.to_set or
+ raise AliasDefinitionError.new(_('The arguments in alias must match the substitutions in command, and vice versa'))
+
+ begin
+ map text, :action => :"alias_handle<#{text}>", :auth_path => 'run'
+ rescue
+ raise AliasDefinitionError.new(_('Error mapping %{text} as command: %{error}') %
+ {:text => text, :error => $!})
+ end
@aliases[text] = command
- map text, :action => :"alias_handle<#{text}>", :auth_path => 'run'
end
def respond_to?(name, include_private=false)
def method_missing(name, *args, &block)
if name.to_s =~ /\Aalias_handle<(.+)>\Z/
+ text = $1
m, params = args
- # messages created by alias handler will have a depth method, which returns the
- # depth of "recursion" caused by the message
- current_depth = if m.respond_to?(:depth) then m.depth else 0 end
- if current_depth > MAX_RECURSION_DEPTH
- m.reply _('The alias seems to have caused infinite recursion. Please examine your alias definitions')
- return
- end
- command = @aliases[$1]
+ command = @aliases[text]
if command
- # create a fake message containing the intended command
- new_msg = PrivMessage.new(@bot, m.server, m.server.user(m.source), m.target,
- command.gsub(/<(\w+)>/) {|arg| params[:"#{$1}"].to_s})
- # tag incremented depth on the message
- class << new_msg
- self
- end.send(:define_method, :depth) {current_depth + 1}
-
- @bot.plugins.privmsg(new_msg)
+ begin
+ # create a fake message containing the intended command
+ @bot.plugins.privmsg fake_message(command.gsub(/<(\w+)>/) {|arg| params[:"#{$1}"].to_s}, :from => m, :delegate => false)
+ rescue RecurseTooDeep
+ m.reply _('The alias seems to have caused infinite recursion. Please examine your alias definitions')
+ return
+ end
else
- m.reply _("Error handling the alias, the command is not defined")
+ m.reply(_("Error handling the alias, The alias %{text} is not defined or has beeen removed. I will stop responding to it after rescan,") %
+ {:text => text})
end
else
super(name, *args, &block)
def help(plugin, topic='')
case topic
when ''
- _('Create and use aliases for commands. Topics: create, commands')
+ if plugin == 'alias' # FIXME find out plugin name programmatically
+ _('Create and use aliases for commands. Topics: create, commands')
+ else
+ # show definition of all aliases whose first word is the parameter of the help
+ # command
+ @aliases.keys.select {|a| a[/\A(\w+)/, 1] == plugin}.map do |a|
+ "#{a} => #{@aliases[a]}"
+ end.join ' | '
+ end
when 'create'
- _('"alias <text> => <command>" => add text as an alias of command. Text can contain placeholders marked with : or * for :words and *multiword arguments. The command can contain placeholders enclosed with < > which will be substituded with argument values. For example: alias googlerbot *terms => google site:linuxbrit.co.uk/rbot/ <terms>')
+ _('"alias <text> => <command>" => add text as an alias of command. Text can contain placeholders marked with : or * for :words and *multiword arguments. The command can contain placeholders enclosed with < > which will be substituded with argument values. For example: alias googlerbot *terms => google site:ruby-rbot.org <terms>')
when 'commands'
_('alias list => list defined aliases | alias whatis <alias> => show definition of the alias | alias remove <alias> => remove defined alias | see the "create" topic about adding aliases')
end
plugin.map 'alias remove *text',
:action => :cmd_remove,
:auth_path => 'edit'
+plugin.map 'alias rm *text',
+ :action => :cmd_remove,
+ :auth_path => 'edit'
plugin.map 'alias *text => *command',
:action => :cmd_add,
:auth_path => 'edit'