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