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