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