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