]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/config.rb
d04546c6afbd61804b02c64b09bf586efc34440c
[user/henk/code/ruby/rbot.git] / lib / rbot / config.rb
1 require 'singleton'
2
3 module Irc
4
5   require 'yaml'
6
7   unless YAML.respond_to?(:load_file)
8       def YAML.load_file( filepath )
9         File.open( filepath ) do |f|
10           YAML::load( f )
11         end
12       end
13   end
14
15   class BotConfigValue
16     # allow the definition order to be preserved so that sorting by
17     # definition order is possible. The BotConfigWizard does this to allow
18     # the :wizard questions to be in a sensible order.
19     @@order = 0
20     attr_reader :type
21     attr_reader :desc
22     attr_reader :key
23     attr_reader :wizard
24     attr_reader :requires_restart
25     attr_reader :requires_rescan
26     attr_reader :order
27     attr_reader :manager
28     attr_reader :auth_path
29     def initialize(key, params)
30       @manager = BotConfig::configmanager
31       # Keys must be in the form 'module.name'.
32       # They will be internally passed around as symbols,
33       # but we accept them both in string and symbol form.
34       unless key.to_s =~ /^.+\..+$/
35         raise ArgumentError,"key must be of the form 'module.name'"
36       end
37       @order = @@order
38       @@order += 1
39       @key = key.to_sym
40       if params.has_key? :default
41         @default = params[:default]
42       else
43         @default = false
44       end
45       @desc = params[:desc]
46       @type = params[:type] || String
47       @on_change = params[:on_change]
48       @validate = params[:validate]
49       @wizard = params[:wizard]
50       @requires_restart = params[:requires_restart]
51       @requires_rescan = params[:requires_rescan]
52       @auth_path = "config::key::#{key.sub('.','::')}"
53     end
54     def default
55       if @default.instance_of?(Proc)
56         @default.call
57       else
58         @default
59       end
60     end
61     def get
62       return @manager.config[@key] if @manager.config.has_key?(@key)
63       return @default
64     end
65     alias :value :get
66     def set(value, on_change = true)
67       @manager.config[@key] = value
68       @manager.changed = true
69       @on_change.call(@manager.bot, value) if on_change && @on_change
70     end
71     def unset
72       @manager.config.delete(@key)
73       @manager.changed = true
74     end
75
76     # set string will raise ArgumentErrors on failed parse/validate
77     def set_string(string, on_change = true)
78       value = parse string
79       if validate value
80         set value, on_change
81       else
82         raise ArgumentError, "invalid value: #{string}"
83       end
84     end
85
86     # override this. the default will work for strings only
87     def parse(string)
88       string
89     end
90
91     def to_s
92       get.to_s
93     end
94
95     private
96     def validate(value)
97       return true unless @validate
98       if @validate.instance_of?(Proc)
99         return @validate.call(value)
100       elsif @validate.instance_of?(Regexp)
101         raise ArgumentError, "validation via Regexp only supported for strings!" unless value.instance_of? String
102         return @validate.match(value)
103       else
104         raise ArgumentError, "validation type #{@validate.class} not supported"
105       end
106     end
107   end
108
109   class BotConfigStringValue < BotConfigValue
110   end
111
112   class BotConfigBooleanValue < BotConfigValue
113     def parse(string)
114       return true if string == "true"
115       return false if string == "false"
116       raise ArgumentError, "#{string} does not match either 'true' or 'false'"
117     end
118   end
119
120   class BotConfigIntegerValue < BotConfigValue
121     def parse(string)
122       raise ArgumentError, "not an integer: #{string}" unless string =~ /^-?\d+$/
123       string.to_i
124     end
125   end
126
127   class BotConfigFloatValue < BotConfigValue
128     def parse(string)
129       raise ArgumentError, "not a float #{string}" unless string =~ /^-?[\d.]+$/
130       string.to_f
131     end
132   end
133
134   class BotConfigArrayValue < BotConfigValue
135     def parse(string)
136       string.split(/,\s+/)
137     end
138     def to_s
139       get.join(", ")
140     end
141     def add(val)
142       curval = self.get
143       set(curval + [val]) unless curval.include?(val)
144     end
145     def rm(val)
146       curval = self.get
147       raise ArgumentError, "value #{val} not present" unless curval.include?(val)
148       set(curval - [val])
149     end
150   end
151
152   class BotConfigEnumValue < BotConfigValue
153     def initialize(key, params)
154       super
155       @values = params[:values]
156     end
157     def values
158       if @values.instance_of?(Proc)
159         return @values.call(@manager.bot)
160       else
161         return @values
162       end
163     end
164     def parse(string)
165       unless values.include?(string)
166         raise ArgumentError, "invalid value #{string}, allowed values are: " + values.join(", ")
167       end
168       string
169     end
170     def desc
171       "#{@desc} [valid values are: " + values.join(", ") + "]"
172     end
173   end
174
175   # container for bot configuration
176   class BotConfigManagerClass
177
178     include Singleton
179
180     attr_reader :bot
181     attr_reader :items
182     attr_reader :config
183     attr_accessor :changed
184
185     def initialize
186       bot_associate(nil,true)
187     end
188
189     def reset_config
190       @items = Hash.new
191       @config = Hash.new(false)
192     end
193
194     # Associate with bot _bot_
195     def bot_associate(bot, reset=false)
196       reset_config if reset
197       @bot = bot
198       return unless @bot
199
200       @changed = false
201       if(File.exist?("#{@bot.botclass}/conf.yaml"))
202         begin
203           newconfig = YAML::load_file("#{@bot.botclass}/conf.yaml")
204           newconfig.each { |key, val|
205             @config[key.to_sym] = val
206           }
207           return
208         rescue
209           error "failed to read conf.yaml: #{$!}"
210         end
211       end
212       # if we got here, we need to run the first-run wizard
213       BotConfigWizard.new(@bot).run
214       # save newly created config
215       @changed = true
216       save
217     end
218
219     def register(item)
220       unless item.kind_of?(BotConfigValue)
221         raise ArgumentError,"item must be a BotConfigValue"
222       end
223       @items[item.key] = item
224     end
225
226     # currently we store values in a hash but this could be changed in the
227     # future. We use hash semantics, however.
228     # components that register their config keys and setup defaults are
229     # supported via []
230     def [](key)
231       # return @items[key].value if @items.has_key?(key)
232       return @items[key.to_sym].value if @items.has_key?(key.to_sym)
233       # try to still support unregistered lookups
234       # but warn about them
235       #      if @config.has_key?(key)
236       #        warning "Unregistered lookup #{key.inspect}"
237       #        return @config[key]
238       #      end
239       if @config.has_key?(key.to_sym)
240         warning "Unregistered lookup #{key.to_sym.inspect}"
241         return @config[key.to_sym]
242       end
243       return false
244     end
245
246     # TODO should I implement this via BotConfigValue or leave it direct?
247     #    def []=(key, value)
248     #    end
249
250     # pass everything else through to the hash
251     def method_missing(method, *args, &block)
252       return @config.send(method, *args, &block)
253     end
254
255     # write current configuration to #{botclass}/conf.yaml
256     def save
257       if not @changed
258         debug "Not writing conf.yaml (unchanged)"
259         return
260       end
261       begin
262         debug "Writing new conf.yaml ..."
263         File.open("#{@bot.botclass}/conf.yaml.new", "w") do |file|
264           savehash = {}
265           @config.each { |key, val|
266             savehash[key.to_s] = val
267           }
268           file.puts savehash.to_yaml
269         end
270         debug "Officializing conf.yaml ..."
271         File.rename("#{@bot.botclass}/conf.yaml.new",
272                     "#{@bot.botclass}/conf.yaml")
273         @changed = false
274       rescue => e
275         error "failed to write configuration file conf.yaml! #{$!}"
276         error "#{e.class}: #{e}"
277         error e.backtrace.join("\n")
278       end
279     end
280   end
281
282   module BotConfig
283     # Returns the only BotConfigManagerClass
284     #
285     def BotConfig.configmanager
286       return BotConfigManagerClass.instance
287     end
288
289     # Register a config value
290     def BotConfig.register(item)
291       BotConfig.configmanager.register(item)
292     end
293   end
294
295   class BotConfigWizard
296     def initialize(bot)
297       @bot = bot
298       @manager = BotConfig::configmanager
299       @questions = @manager.items.values.find_all {|i| i.wizard }
300     end
301
302     def run()
303       puts "First time rbot configuration wizard"
304       puts "===================================="
305       puts "This is the first time you have run rbot with a config directory of:"
306       puts @bot.botclass
307       puts "This wizard will ask you a few questions to get you started."
308       puts "The rest of rbot's configuration can be manipulated via IRC once"
309       puts "rbot is connected and you are auth'd."
310       puts "-----------------------------------"
311
312       return unless @questions
313       @questions.sort{|a,b| a.order <=> b.order }.each do |q|
314         puts q.desc
315         begin
316           print q.key.to_s + " [#{q.to_s}]: "
317           response = STDIN.gets
318           response.chop!
319           unless response.empty?
320             q.set_string response, false
321           end
322           puts "configured #{q.key} => #{q.to_s}"
323           puts "-----------------------------------"
324         rescue ArgumentError => e
325           puts "failed to set #{q.key}: #{e.message}"
326           retry
327         end
328       end
329     end
330   end
331
332 end