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