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