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