]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/alias.rb
89be0020434b81e1593a40d573aa77db6a80592e
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / alias.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: Alias plugin for rbot
5 #
6 # Author:: Yaohan Chen <yaohan.chen@gmail.com>
7 # Copyright:: (C) 2007 Yaohan Chen
8 # License:: GPLv2
9 #
10 # This plugin allows defining aliases for rbot commands. Aliases are like normal rbot
11 # commands and can take parameters. When called, they will be substituted into an
12 # exisitng rbot command and that is run.
13 #
14 # == Example Session
15 #   < alias googlerbot *terms => google site:ruby-rbot.org <terms>
16 #   > okay
17 #   < googlerbot plugins
18 #   > Results for site:ruby-rbot.org plugins: ....
19 #
20 # == Security
21 # By default, only the owner can define and remove aliases, while everyone else can
22 # use and view them. When a command is executed with an alias, it's mapped normally with
23 # the alias user appearing to attempt to execute the command. Therefore it should be not
24 # possible to use aliases to circumvent permission sets. Care should be taken when
25 # defining aliases, due to these concerns:
26 # * Defined aliases can potentially override other plugins' maps, if this plugin is
27 #   loaded first
28 # * Aliases can cause infinite recursion of aliases and/or commands. The plugin attempts
29 #   to detect and stop this, but a few recursive calls can still cause spamming
30
31 require 'yaml'
32 require 'set'
33
34 class AliasPlugin < Plugin
35   # an exception raised when loading or getting input of invalid alias definitions
36   class AliasDefinitionError < ArgumentError
37   end
38
39   def initialize
40     super
41     @data_path = datafile
42     @data_file = File.join(@data_path, 'aliases.yaml')
43     # hash of alias => command entries
44     data = nil
45     aliases = if File.exist?(@data_file) &&
46                  (data = YAML.load_file(@data_file)) &&
47                  data.respond_to?(:each_pair)
48                 data
49               else
50                 warning _("Data file is not found or corrupt, reinitializing data")
51                 Hash.new
52                end
53     @aliases = Hash.new
54     aliases.each_pair do |a, c|
55       begin
56         add_alias(a, c)
57       rescue AliasDefinitionError
58         warning _("Invalid alias entry %{alias} : %{command} in %{filename}: %{reason}") %
59                 {:alias => a, :command => c, :filename => @data_file, :reason => $1}
60       end
61     end
62   end
63
64   def save
65     FileUtils.mkdir_p(@data_path)
66     Utils.safe_save(@data_file) {|f| f.write @aliases.to_yaml}
67   end
68
69   def cmd_add(m, params)
70     begin
71       add_alias(params[:text].to_s, params[:command].to_s)
72       m.okay
73     rescue AliasDefinitionError
74       m.reply _('The definition you provided is invalid: %{reason}') % {:reason => $!}
75     end
76   end
77
78   def cmd_remove(m, params)
79     text = params[:text].to_s
80     if @aliases.has_key?(text)
81       @aliases.delete(text)
82       # TODO when rbot supports it, remove the mapping corresponding to the alias
83       m.okay
84     else
85       m.reply _('No such alias is defined')
86     end
87   end
88
89   def cmd_list(m, params)
90     if @aliases.empty?
91       m.reply _('No aliases defined')
92     else
93       m.reply @aliases.map {|a, c| "#{a} => #{c}"}.join(' | ')
94     end
95   end
96
97   def cmd_whatis(m, params)
98     text = params[:text].to_s
99     if @aliases.has_key?(text)
100       m.reply _('Alias of %{command}') % {:command => @aliases[text]}
101     else
102       m.reply _('No such alias is defined')
103     end
104   end
105
106   def add_alias(text, command)
107     # each alias is implemented by adding a message map, whose handler creates a message
108     # containing the aliased command
109
110     command.scan(/<(\w+)>/).flatten.to_set ==
111       text.split.grep(/\A[:*](\w+)\Z/) {$1}.to_set or
112       raise AliasDefinitionError.new(_('The arguments in alias must match the substitutions in command, and vice versa'))
113
114     begin
115       map text, :action => :"alias_handle<#{text}>", :auth_path => 'run'
116     rescue
117       raise AliasDefinitionError.new(_('Error mapping %{text} as command: %{error}') %
118                                      {:text => text, :error => $!})
119     end
120     @aliases[text] = command
121   end
122
123   def respond_to?(name, include_private=false)
124     name.to_s =~ /\Aalias_handle<.+>\Z/ || super
125   end
126
127   def method_missing(name, *args, &block)
128     if name.to_s =~ /\Aalias_handle<(.+)>\Z/
129       text = $1
130       m, params = args
131
132       command = @aliases[text]
133       if command
134         begin
135           # create a fake message containing the intended command
136           @bot.plugins.privmsg fake_message(command.gsub(/<(\w+)>/) {|arg| params[:"#{$1}"].to_s}, :from => m, :delegate => false)
137         rescue RecurseTooDeep
138           m.reply _('The alias seems to have caused infinite recursion. Please examine your alias definitions')
139           return
140         end
141       else
142         m.reply(_("Error handling the alias, The alias %{text} is not defined or has beeen removed. I will stop responding to it after rescan,") %
143                 {:text => text})
144       end
145     else
146       super(name, *args, &block)
147     end
148   end
149
150   def help(plugin, topic='')
151     case topic
152     when ''
153       if plugin == 'alias' # FIXME find out plugin name programmatically
154         _('Create and use aliases for commands. Topics: create, commands')
155       else
156         # show definition of all aliases whose first word is the parameter of the help
157         # command
158         @aliases.keys.select {|a| a[/\A(\w+)/, 1] == plugin}.map do |a|
159           "#{a} => #{@aliases[a]}"
160         end.join ' | '
161       end
162     when 'create'
163       _('"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>')
164     when 'commands'
165       _('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')
166     end
167   end
168 end
169
170 plugin = AliasPlugin.new
171 plugin.default_auth('edit', false)
172 plugin.default_auth('run', true)
173 plugin.default_auth('list', true)
174
175 plugin.map 'alias list',
176            :action => :cmd_list,
177            :auth_path => 'view'
178 plugin.map 'alias whatis *text',
179            :action => :cmd_whatis,
180            :auth_path => 'view'
181 plugin.map 'alias remove *text',
182            :action => :cmd_remove,
183            :auth_path => 'edit'
184 plugin.map 'alias rm *text',
185            :action => :cmd_remove,
186            :auth_path => 'edit'
187 plugin.map 'alias *text => *command',
188            :action => :cmd_add,
189            :auth_path => 'edit'
190
191
192