]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/config.rb
chucknorris: fix loading
[user/henk/code/ruby/rbot.git] / lib / rbot / config.rb
1 require 'singleton'
2
3 require 'yaml'
4
5 unless YAML.respond_to?(:load_file)
6   def YAML.load_file( filepath )
7     File.open( filepath ) do |f|
8       YAML::load( f )
9     end
10   end
11 end
12
13
14 module Irc
15
16 class Bot
17 module Config
18   class Value
19     # allow the definition order to be preserved so that sorting by
20     # definition order is possible. The Wizard does this to allow
21     # the :wizard questions to be in a sensible order.
22     @@order = 0
23     attr_reader :type
24     attr_reader :desc
25     attr_reader :key
26     attr_reader :wizard
27     attr_reader :store_default
28     attr_reader :requires_restart
29     attr_reader :requires_rescan
30     attr_reader :order
31     attr_reader :manager
32     attr_reader :auth_path
33     def initialize(key, params)
34       @manager = Config.manager
35       # Keys must be in the form 'module.name'.
36       # They will be internally passed around as symbols,
37       # but we accept them both in string and symbol form.
38       unless key.to_s =~ /^.+\..+$/
39         raise ArgumentError,"key must be of the form 'module.name'"
40       end
41       @order = @@order
42       @@order += 1
43       @key = key.to_sym
44       if @manager.overrides.key?(@key)
45         @default = @manager.overrides[@key]
46       elsif params.has_key? :default
47         @default = params[:default]
48       else
49         @default = false
50       end
51       @desc = params[:desc]
52       @type = params[:type] || String
53       @on_change = params[:on_change]
54       @validate = params[:validate]
55       @wizard = params[:wizard]
56       @store_default = params[:store_default]
57       @requires_restart = params[:requires_restart]
58       @requires_rescan = params[:requires_rescan]
59       @auth_path = "config::key::#{key.sub('.','::')}"
60     end
61     def default
62       if @default.instance_of?(Proc)
63         @default.call
64       else
65         @default
66       end
67     end
68     def get
69       return @manager.config[@key] if @manager.config.has_key?(@key)
70       return default
71     end
72     alias :value :get
73     def set(value, on_change = true)
74       @manager.config[@key] = value
75       @manager.changed = true
76       @on_change.call(@manager.bot, value) if on_change && @on_change
77       return self
78     end
79     def unset
80       @manager.config.delete(@key)
81       @manager.changed = true
82       @on_change.call(@manager.bot, value) if @on_change
83       return self
84     end
85
86     # set string will raise ArgumentErrors on failed parse/validate
87     def set_string(string, on_change = true)
88       value = parse string
89       if validate value
90         set value, on_change
91       else
92         raise ArgumentError, "invalid value: #{string}"
93       end
94     end
95
96     # override this. the default will work for strings only
97     def parse(string)
98       string
99     end
100
101     def to_s
102       get.to_s
103     end
104
105     protected
106     def validate(val, validator  = @validate)
107       case validator
108       when false, nil
109         return true
110       when Proc
111         return validator.call(val)
112       when Regexp
113         raise ArgumentError, "validation via Regexp only supported for strings!" unless String === val
114         return validator.match(val)
115       else
116         raise ArgumentError, "validation type #{validator.class} not supported"
117       end
118     end
119   end
120
121   class StringValue < Value
122   end
123
124   class BooleanValue < Value
125     def parse(string)
126       return true if string == "true"
127       return false if string == "false"
128       if string =~ /^-?\d+$/
129         return string.to_i != 0
130       end
131       raise ArgumentError, "#{string} does not match either 'true' or 'false', and it's not an integer either"
132     end
133     def get
134       r = super
135       if r.kind_of?(Integer)
136         return r != 0
137       else
138         return r
139       end
140     end
141   end
142
143   class IntegerValue < Value
144     def parse(string)
145       return 1 if string == "true"
146       return 0 if string == "false"
147       raise ArgumentError, "not an integer: #{string}" unless string =~ /^-?\d+$/
148       string.to_i
149     end
150     def get
151       r = super
152       if r.kind_of?(Integer)
153         return r
154       else
155         return r ? 1 : 0
156       end
157     end
158   end
159
160   class FloatValue < Value
161     def parse(string)
162       raise ArgumentError, "not a float #{string}" unless string =~ /^-?[\d.]+$/
163       string.to_f
164     end
165   end
166
167   class ArrayValue < Value
168     def initialize(key, params)
169       super
170       @validate_item = params[:validate_item]
171       @validate ||= Proc.new do |v|
172         !v.find { |i| !validate_item(i) }
173       end
174     end
175
176     def validate_item(item)
177       validate(item, @validate_item)
178     end
179
180     def parse(string)
181       string.split(/,\s+/)
182     end
183     def to_s
184       get.join(", ")
185     end
186     def add(val)
187       newval = self.get.dup
188       unless newval.include? val
189         newval << val
190         validate_item(val) or raise ArgumentError, "invalid item: #{val}"
191         validate(newval) or raise ArgumentError, "invalid value: #{newval.inspect}"
192         set(newval)
193       end
194     end
195     def rm(val)
196       curval = self.get
197       raise ArgumentError, "value #{val} not present" unless curval.include?(val)
198       set(curval - [val])
199     end
200   end
201
202   class EnumValue < Value
203     def initialize(key, params)
204       super
205       @values = params[:values]
206     end
207     def values
208       if @values.instance_of?(Proc)
209         return @values.call(@manager.bot)
210       else
211         return @values
212       end
213     end
214     def parse(string)
215       unless values.include?(string)
216         raise ArgumentError, "invalid value #{string}, allowed values are: " + values.join(", ")
217       end
218       string
219     end
220     def desc
221       _("%{desc} [valid values are: %{values}]") % {:desc => @desc, :values => values.join(', ')}
222     end
223   end
224
225   # container for bot configuration
226   class ManagerClass
227
228     include Singleton
229
230     attr_reader :bot
231     attr_reader :items
232     attr_reader :config
233     attr_reader :overrides
234     attr_accessor :changed
235
236     def initialize
237       bot_associate(nil,true)
238     end
239
240     def reset_config
241       @items = Hash.new
242       @config = Hash.new(false)
243
244       # We allow default values for config keys to be overridden by
245       # the config file /etc/rbot.conf
246       # The main purpose for this is to allow distro or system-wide
247       # settings such as external program paths (figlet, toilet, ispell)
248       # to be set once for all the bots.
249       @overrides = Hash.new
250       etcfile = "/etc/rbot.conf"
251       if File.exist?(etcfile)
252         log "Loading defaults from #{etcfile}"
253         etcconf = YAML::load_file(etcfile)
254         etcconf.each { |k, v|
255           @overrides[k.to_sym] = v
256         }
257       end
258     end
259
260     # Associate with bot _bot_
261     def bot_associate(bot, reset=false)
262       reset_config if reset
263       @bot = bot
264       return unless @bot
265
266       @changed = false
267       conf = @bot.path 'conf.yaml'
268       if File.exist? conf
269         begin
270           newconfig = YAML::load_file conf
271           newconfig.each { |key, val|
272             @config[key.to_sym] = val
273           }
274           return
275         rescue
276           error "failed to read conf.yaml: #{$!}"
277         end
278       end
279       # config options with :store_default to true should store
280       # their default value at first run.
281       # Some defaults might change anytime the bot starts
282       #  for instance core.db or authpw
283       @items.values.find_all {|i| i.store_default }.each do |value|
284         @config[value.key] = value.default
285       end
286
287       # if we got here, we need to run the first-run wizard
288       Wizard.new(@bot).run
289       # save newly created config
290       @changed = true
291       save
292     end
293
294     def register(item)
295       unless item.kind_of?(Value)
296         raise ArgumentError,"item must be an Irc::Bot::Config::Value"
297       end
298       @items[item.key] = item
299     end
300
301     # currently we store values in a hash but this could be changed in the
302     # future. We use hash semantics, however.
303     # components that register their config keys and setup defaults are
304     # supported via []
305     def [](key)
306       # return @items[key].value if @items.has_key?(key)
307       return @items[key.to_sym].value if @items.has_key?(key.to_sym)
308       # try to still support unregistered lookups
309       # but warn about them
310       #      if @config.has_key?(key)
311       #        warning "Unregistered lookup #{key.inspect}"
312       #        return @config[key]
313       #      end
314       if @config.has_key?(key.to_sym)
315         warning _("Unregistered lookup #{key.to_sym.inspect}")
316         return @config[key.to_sym]
317       end
318       return false
319     end
320
321     def []=(key, value)
322       return @items[key.to_sym].set(value) if @items.has_key?(key.to_sym)
323       if @config.has_key?(key.to_sym)
324         warning _("Unregistered lookup #{key.to_sym.inspect}")
325         return @config[key.to_sym] = value
326       end
327     end
328
329     # pass everything else through to the hash
330     def method_missing(method, *args, &block)
331       return @config.send(method, *args, &block)
332     end
333
334     # write current configuration to #{botclass}/conf.yaml
335     def save
336       if not @changed
337         debug "Not writing conf.yaml (unchanged)"
338         return
339       end
340       begin
341         conf = @bot.path 'conf.yaml'
342         fnew = conf + '.new'
343         debug "Writing new conf.yaml ..."
344         File.open(fnew, "w") do |file|
345           savehash = {}
346           @config.each { |key, val|
347             savehash[key.to_s] = val
348           }
349           file.puts savehash.to_yaml
350         end
351         debug "Officializing conf.yaml ..."
352         File.rename(fnew, conf)
353         @changed = false
354       rescue => e
355         error "failed to write configuration file conf.yaml! #{$!}"
356         error "#{e.class}: #{e}"
357         error e.backtrace.join("\n")
358       end
359     end
360   end
361
362   # Returns the only Irc::Bot::Config::ManagerClass
363   #
364   def Config.manager
365     return ManagerClass.instance
366   end
367
368   # Register a config value
369   def Config.register(item)
370     Config.manager.register(item)
371   end
372
373   class Wizard
374     def initialize(bot)
375       @bot = bot
376       @manager = Config.manager
377       @questions = @manager.items.values.find_all {|i| i.wizard }
378     end
379
380     def run()
381       $stdout.sync = true
382       puts _("First time rbot configuration wizard")
383       puts "===================================="
384       puts _("This is the first time you have run rbot with a config directory of: #{@bot.botclass}")
385       puts _("This wizard will ask you a few questions to get you started.")
386       puts _("The rest of rbot's configuration can be manipulated via IRC once rbot is connected and you are auth'd.")
387       puts "-----------------------------------"
388
389       return unless @questions
390       @questions.sort{|a,b| a.order <=> b.order }.each do |q|
391         puts _(q.desc)
392         begin
393           print q.key.to_s + " [#{q.to_s}]: "
394           response = STDIN.gets
395           response.chop!
396           unless response.empty?
397             q.set_string response, false
398           end
399           puts _("configured #{q.key} => #{q.to_s}")
400           puts "-----------------------------------"
401         rescue ArgumentError => e
402           puts _("failed to set #{q.key}: #{e.message}")
403           retry
404         end
405       end
406     end
407   end
408
409 end
410 end
411 end