4 require 'rbot/messagemapper'
6 unless YAML.respond_to?(:load_file)
7 def YAML.load_file( filepath )
8 File.open( filepath ) do |f|
15 # allow the definition order to be preserved so that sorting by
16 # definition order is possible. The BotConfigWizard does this to allow
17 # the :wizard questions to be in a sensible order.
23 attr_reader :requires_restart
25 def initialize(key, params)
26 unless key =~ /^.+\..+$/
27 raise ArgumentError,"key must be of the form 'module.name'"
32 if params.has_key? :default
33 @default = params[:default]
38 @type = params[:type] || String
39 @on_change = params[:on_change]
40 @validate = params[:validate]
41 @wizard = params[:wizard]
42 @requires_restart = params[:requires_restart]
45 if @default.instance_of?(Proc)
52 return BotConfig.config[@key] if BotConfig.config.has_key?(@key)
56 def set(value, on_change = true)
57 BotConfig.config[@key] = value
58 @on_change.call(BotConfig.bot, value) if on_change && @on_change
61 BotConfig.config.delete(@key)
64 # set string will raise ArgumentErrors on failed parse/validate
65 def set_string(string, on_change = true)
70 raise ArgumentError, "invalid value: #{string}"
74 # override this. the default will work for strings only
85 return true unless @validate
86 if @validate.instance_of?(Proc)
87 return @validate.call(value)
88 elsif @validate.instance_of?(Regexp)
89 raise ArgumentError, "validation via Regexp only supported for strings!" unless value.instance_of? String
90 return @validate.match(value)
92 raise ArgumentError, "validation type #{@validate.class} not supported"
97 class BotConfigStringValue < BotConfigValue
99 class BotConfigBooleanValue < BotConfigValue
101 return true if string == "true"
102 return false if string == "false"
103 raise ArgumentError, "#{string} does not match either 'true' or 'false'"
106 class BotConfigIntegerValue < BotConfigValue
108 raise ArgumentError, "not an integer: #{string}" unless string =~ /^-?\d+$/
112 class BotConfigFloatValue < BotConfigValue
114 raise ArgumentError, "not a float #{string}" unless string =~ /^-?[\d.]+$/
118 class BotConfigArrayValue < BotConfigValue
126 class BotConfigEnumValue < BotConfigValue
127 def initialize(key, params)
129 @values = params[:values]
132 if @values.instance_of?(Proc)
133 return @values.call(BotConfig.bot)
139 unless @values.include?(string)
140 raise ArgumentError, "invalid value #{string}, allowed values are: " + @values.join(", ")
145 "#{@desc} [valid values are: " + values.join(", ") + "]"
149 # container for bot configuration
151 # Array of registered BotConfigValues for defaults, types and help
156 # Hash containing key => value pairs for lookup and serialisation
157 @@config = Hash.new(false)
165 def BotConfig.register(item)
166 unless item.kind_of?(BotConfigValue)
167 raise ArgumentError,"item must be a BotConfigValue"
169 @@items[item.key] = item
172 # currently we store values in a hash but this could be changed in the
173 # future. We use hash semantics, however.
174 # components that register their config keys and setup defaults are
177 return @@items[key].value if @@items.has_key?(key)
178 # try to still support unregistered lookups
179 return @@config[key] if @@config.has_key?(key)
183 # TODO should I implement this via BotConfigValue or leave it direct?
184 # def []=(key, value)
187 # pass everything else through to the hash
188 def method_missing(method, *args, &block)
189 return @@config.send(method, *args, &block)
192 def handle_list(m, params)
195 @@items.each_key do |key|
196 mod, name = key.split('.')
197 next unless mod == params[:module]
198 modules.push key unless modules.include?(name)
201 m.reply "no such module #{params[:module]}"
203 m.reply modules.join(", ")
206 @@items.each_key do |key|
207 name = key.split('.').first
208 modules.push name unless modules.include?(name)
210 m.reply "modules: " + modules.join(", ")
214 def handle_get(m, params)
216 unless @@items.has_key?(key)
217 m.reply "no such config key #{key}"
220 value = @@items[key].to_s
221 m.reply "#{key}: #{value}"
224 def handle_desc(m, params)
226 unless @@items.has_key?(key)
227 m.reply "no such config key #{key}"
229 puts @@items[key].inspect
230 m.reply "#{key}: #{@@items[key].desc}"
233 def handle_unset(m, params)
235 unless @@items.has_key?(key)
236 m.reply "no such config key #{key}"
239 handle_get(m, params)
242 def handle_set(m, params)
244 value = params[:value].to_s
245 unless @@items.has_key?(key)
246 m.reply "no such config key #{key}"
250 @@items[key].set_string(value)
251 rescue ArgumentError => e
252 m.reply "failed to set #{key}: #{e.message}"
255 if @@items[key].requires_restart
256 m.reply "this config change will take effect on the next restart"
262 def handle_help(m, params)
263 topic = params[:topic]
266 m.reply "config module - bot configuration. usage: list, desc, get, set, unset"
268 m.reply "config list => list configuration modules, config list <module> => list configuration keys for module <module>"
270 m.reply "config get <key> => get configuration value for key <key>"
272 m.reply "reset key <key> to the default"
274 m.reply "config set <key> <value> => set configuration value for key <key> to <value>"
276 m.reply "config desc <key> => describe what key <key> configures"
278 m.reply "no help for config #{topic}"
282 m.reply "incorrect usage, try '#{@@bot.nick}: help config'"
285 # bot:: parent bot class
286 # create a new config hash from #{botclass}/conf.rbot
290 # respond to config messages, to provide runtime configuration
300 @handler = MessageMapper.new(self)
301 @handler.map 'config list :module', :action => 'handle_list',
302 :defaults => {:module => false}
303 @handler.map 'config get :key', :action => 'handle_get'
304 @handler.map 'config desc :key', :action => 'handle_desc'
305 @handler.map 'config describe :key', :action => 'handle_desc'
306 @handler.map 'config set :key *value', :action => 'handle_set'
307 @handler.map 'config unset :key', :action => 'handle_unset'
308 @handler.map 'config help :topic', :action => 'handle_help',
309 :defaults => {:topic => false}
310 @handler.map 'help config :topic', :action => 'handle_help',
311 :defaults => {:topic => false}
313 if(File.exist?("#{@@bot.botclass}/conf.yaml"))
315 newconfig = YAML::load_file("#{@@bot.botclass}/conf.yaml")
316 @@config.update newconfig
319 $stderr.puts "failed to read conf.yaml: #{$!}"
322 # if we got here, we need to run the first-run wizard
323 BotConfigWizard.new(@@bot).run
324 # save newly created config
328 # write current configuration to #{botclass}/conf.rbot
331 File.open("#{@@bot.botclass}/conf.yaml.new", "w") do |file|
332 file.puts @@config.to_yaml
334 File.rename("#{@@bot.botclass}/conf.yaml.new",
335 "#{@@bot.botclass}/conf.yaml")
337 $stderr.puts "failed to write configuration file conf.yaml! #{$!}"
346 class BotConfigWizard
349 @questions = BotConfig.items.values.find_all {|i| i.wizard }
353 puts "First time rbot configuration wizard"
354 puts "===================================="
355 puts "This is the first time you have run rbot with a config directory of:"
357 puts "This wizard will ask you a few questions to get you started."
358 puts "The rest of rbot's configuration can be manipulated via IRC once"
359 puts "rbot is connected and you are auth'd."
360 puts "-----------------------------------"
362 return unless @questions
363 @questions.sort{|a,b| a.order <=> b.order }.each do |q|
366 print q.key + " [#{q.to_s}]: "
367 response = STDIN.gets
369 unless response.empty?
370 q.set_string response, false
372 puts "configured #{q.key} => #{q.to_s}"
373 puts "-----------------------------------"
374 rescue ArgumentError => e
375 puts "failed to set #{q.key}: #{e.message}"