#-- vim:sw=2:et #++ # # :title: Alias plugin for rbot # # Author:: Yaohan Chen # Copyright:: (C) 2007 Yaohan Chen # License:: GPLv2 # # This plugin allows defining aliases for rbot commands. Aliases are like normal rbot # commands and can take parameters. When called, they will be substituted into an # exisitng rbot command and that is run. # # == Example Session # < alias googlerbot *terms => google site:ruby-rbot.org # > okay # < googlerbot plugins # > Results for site:ruby-rbot.org plugins: .... # # == Security # By default, only the owner can define and remove aliases, while everyone else can # use and view them. When a command is executed with an alias, it's mapped normally with # the alias user appearing to attempt to execute the command. Therefore it should be not # possible to use aliases to circumvent permission sets. Care should be taken when # defining aliases, due to these concerns: # * Defined aliases can potentially override other plugins' maps, if this plugin is # loaded first # * Aliases can cause infinite recursion of aliases and/or commands. The plugin attempts # 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 def initialize super @data_path = "#{@bot.botclass}/alias/" @data_file = "#{@data_path}/aliases.yaml" # hash of alias => command entries 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 = 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 def save FileUtils.mkdir_p(@data_path) Utils.safe_save(@data_file) {|f| f.write @aliases.to_yaml} end def cmd_add(m, params) begin add_alias(params[:text].to_s, params[:command].to_s) m.okay rescue AliasDefinitionError m.reply _('The definition you provided is invalid: %{reason}') % {:reason => $!} end end def cmd_remove(m, params) text = params[:text].to_s if @aliases.has_key?(text) @aliases.delete(text) # TODO when rbot supports it, remove the mapping corresponding to the alias m.okay else m.reply _('No such alias is defined') end end def cmd_list(m, params) if @aliases.empty? m.reply _('No aliases defined') else m.reply @aliases.map {|a, c| "#{a} => #{c}"}.join(' | ') end end def cmd_whatis(m, params) text = params[:text].to_s if @aliases.has_key?(text) m.reply _('Alias of %{command}') % {:command => @aliases[text]} else m.reply _('No such alias is defined') end end def add_alias(text, command) # each alias is implemented by adding a message map, whose handler creates a message # containing the aliased command 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 end def respond_to?(name, include_private=false) name.to_s =~ /\Aalias_handle<.+>\Z/ || super end def method_missing(name, *args, &block) if name.to_s =~ /\Aalias_handle<(.+)>\Z/ text = $1 m, params = args command = @aliases[text] if command 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 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) end end def help(plugin, topic='') case topic when '' 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 => " => 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 ') when 'commands' _('alias list => list defined aliases | alias whatis => show definition of the alias | alias remove => remove defined alias | see the "create" topic about adding aliases') end end end plugin = AliasPlugin.new plugin.default_auth('edit', false) plugin.default_auth('run', true) plugin.default_auth('list', true) plugin.map 'alias list', :action => :cmd_list, :auth_path => 'view' plugin.map 'alias whatis *text', :action => :cmd_whatis, :auth_path => 'view' 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'