]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/config.rb
Thu Aug 04 23:03:30 BST 2005 Tom Gilbert <tom@linuxbrit.co.uk>
[user/henk/code/ruby/rbot.git] / lib / rbot / config.rb
1 module Irc
2
3   require 'yaml'
4   require 'rbot/messagemapper'
5
6   class BotConfigValue
7     # allow the definition order to be preserved so that sorting by
8     # definition order is possible. The BotConfigWizard does this to allow
9     # the :wizard questions to be in a sensible order.
10     @@order = 0
11     attr_reader :type
12     attr_reader :desc
13     attr_reader :key
14     attr_reader :wizard
15     attr_reader :requires_restart
16     attr_reader :order
17     def initialize(key, params)
18       unless key =~ /^.+\..+$/
19         raise ArgumentError,"key must be of the form 'module.name'"
20       end
21       @order = @@order
22       @@order += 1
23       @key = key
24       if params.has_key? :default
25         @default = params[:default]
26       else
27         @default = false
28       end
29       @desc = params[:desc]
30       @type = params[:type] || String
31       @on_change = params[:on_change]
32       @validate = params[:validate]
33       @wizard = params[:wizard]
34       @requires_restart = params[:requires_restart]
35     end
36     def default
37       if @default.instance_of?(Proc)
38         @default.call
39       else
40         @default
41       end
42     end
43     def get
44       return BotConfig.config[@key] if BotConfig.config.has_key?(@key)
45       return @default
46     end
47     alias :value :get
48     def set(value, on_change = true)
49       BotConfig.config[@key] = value
50       @on_change.call(BotConfig.bot, value) if on_change && @on_change
51     end
52     def unset
53       BotConfig.config.delete(@key)
54     end
55
56     # set string will raise ArgumentErrors on failed parse/validate
57     def set_string(string, on_change = true)
58       value = parse string
59       if validate value
60         set value, on_change
61       else
62         raise ArgumentError, "invalid value: #{string}"
63       end
64     end
65     
66     # override this. the default will work for strings only
67     def parse(string)
68       string
69     end
70
71     def to_s
72       get.to_s
73     end
74
75     private
76     def validate(value)
77       return true unless @validate
78       if @validate.instance_of?(Proc)
79         return @validate.call(value)
80       elsif @validate.instance_of?(Regexp)
81         raise ArgumentError, "validation via Regexp only supported for strings!" unless value.instance_of? String
82         return @validate.match(value)
83       else
84         raise ArgumentError, "validation type #{@validate.class} not supported"
85       end
86     end
87   end
88
89   class BotConfigStringValue < BotConfigValue
90   end
91   class BotConfigBooleanValue < BotConfigValue
92     def parse(string)
93       return true if string == "true"
94       return false if string == "false"
95       raise ArgumentError, "#{string} does not match either 'true' or 'false'"
96     end
97   end
98   class BotConfigIntegerValue < BotConfigValue
99     def parse(string)
100       raise ArgumentError, "not an integer: #{string}" unless string =~ /^-?\d+$/
101       string.to_i
102     end
103   end
104   class BotConfigFloatValue < BotConfigValue
105     def parse(string)
106       raise ArgumentError, "not a float #{string}" unless string =~ /^-?[\d.]+$/
107       string.to_f
108     end
109   end
110   class BotConfigArrayValue < BotConfigValue
111     def parse(string)
112       string.split(/,\s+/)
113     end
114     def to_s
115       get.join(", ")
116     end
117   end
118   class BotConfigEnumValue < BotConfigValue
119     def initialize(key, params)
120       super
121       @values = params[:values]
122     end
123     def values
124       if @values.instance_of?(Proc)
125         return @values.call(BotConfig.bot)
126       else
127         return @values
128       end
129     end
130     def parse(string)
131       unless @values.include?(string)
132         raise ArgumentError, "invalid value #{string}, allowed values are: " + @values.join(", ")
133       end
134       string
135     end
136     def desc
137       "#{@desc} [valid values are: " + values.join(", ") + "]"
138     end
139   end
140
141   # container for bot configuration
142   class BotConfig
143     # Array of registered BotConfigValues for defaults, types and help
144     @@items = Hash.new
145     def BotConfig.items
146       @@items
147     end
148     # Hash containing key => value pairs for lookup and serialisation
149     @@config = Hash.new(false)
150     def BotConfig.config
151       @@config
152     end
153     def BotConfig.bot
154       @@bot
155     end
156     
157     def BotConfig.register(item)
158       unless item.kind_of?(BotConfigValue)
159         raise ArgumentError,"item must be a BotConfigValue"
160       end
161       @@items[item.key] = item
162     end
163
164     # currently we store values in a hash but this could be changed in the
165     # future. We use hash semantics, however.
166     # components that register their config keys and setup defaults are
167     # supported via []
168     def [](key)
169       return @@items[key].value if @@items.has_key?(key)
170       # try to still support unregistered lookups
171       return @@config[key] if @@config.has_key?(key)
172       return false
173     end
174
175     # TODO should I implement this via BotConfigValue or leave it direct?
176     #    def []=(key, value)
177     #    end
178     
179     # pass everything else through to the hash
180     def method_missing(method, *args, &block)
181       return @@config.send(method, *args, &block)
182     end
183
184     def handle_list(m, params)
185       modules = []
186       if params[:module]
187         @@items.each_key do |key|
188           mod, name = key.split('.')
189           next unless mod == params[:module]
190           modules.push key unless modules.include?(name)
191         end
192         if modules.empty?
193           m.reply "no such module #{params[:module]}"
194         else
195           m.reply modules.join(", ")
196         end
197       else
198         @@items.each_key do |key|
199           name = key.split('.').first
200           modules.push name unless modules.include?(name)
201         end
202         m.reply "modules: " + modules.join(", ")
203       end
204     end
205
206     def handle_get(m, params)
207       key = params[:key]
208       unless @@items.has_key?(key)
209         m.reply "no such config key #{key}"
210         return
211       end
212       value = @@items[key].to_s
213       m.reply "#{key}: #{value}"
214     end
215
216     def handle_desc(m, params)
217       key = params[:key]
218       unless @@items.has_key?(key)
219         m.reply "no such config key #{key}"
220       end
221       puts @@items[key].inspect
222       m.reply "#{key}: #{@@items[key].desc}"
223     end
224
225     def handle_unset(m, params)
226       key = params[:key]
227       unless @@items.has_key?(key)
228         m.reply "no such config key #{key}"
229       end
230       @@items[key].unset
231       handle_get(m, params)
232     end
233
234     def handle_set(m, params)
235       key = params[:key]
236       value = params[:value].to_s
237       unless @@items.has_key?(key)
238         m.reply "no such config key #{key}"
239         return
240       end
241       begin
242         @@items[key].set_string(value)
243       rescue ArgumentError => e
244         m.reply "failed to set #{key}: #{e.message}"
245         return
246       end
247       if @@items[key].requires_restart
248         m.reply "this config change will take effect on the next restart"
249       else
250         m.okay
251       end
252     end
253
254     def handle_help(m, params)
255       topic = params[:topic]
256       case topic
257       when false
258         m.reply "config module - bot configuration. usage: list, desc, get, set, unset"
259       when "list"
260         m.reply "config list => list configuration modules, config list <module> => list configuration keys for module <module>"
261       when "get"
262         m.reply "config get <key> => get configuration value for key <key>"
263       when "unset"
264         m.reply "reset key <key> to the default"
265       when "set"
266         m.reply "config set <key> <value> => set configuration value for key <key> to <value>"
267       when "desc"
268         m.reply "config desc <key> => describe what key <key> configures"
269       else
270         m.reply "no help for config #{topic}"
271       end
272     end
273     def usage(m,params)
274       m.reply "incorrect usage, try '#{@@bot.nick}: help config'"
275     end
276
277     # bot:: parent bot class
278     # create a new config hash from #{botclass}/conf.rbot
279     def initialize(bot)
280       @@bot = bot
281
282       # respond to config messages, to provide runtime configuration
283       # management
284       # messages will be:
285       #  get
286       #  set
287       #  unset
288       #  desc
289       #  and for arrays:
290       #    add TODO
291       #    remove TODO
292       @handler = MessageMapper.new(self)
293       @handler.map 'config list :module', :action => 'handle_list',
294                    :defaults => {:module => false}
295       @handler.map 'config get :key', :action => 'handle_get'
296       @handler.map 'config desc :key', :action => 'handle_desc'
297       @handler.map 'config describe :key', :action => 'handle_desc'
298       @handler.map 'config set :key *value', :action => 'handle_set'
299       @handler.map 'config unset :key', :action => 'handle_unset'
300       @handler.map 'config help :topic', :action => 'handle_help',
301                    :defaults => {:topic => false}
302       @handler.map 'help config :topic', :action => 'handle_help',
303                    :defaults => {:topic => false}
304       
305       if(File.exist?("#{@@bot.botclass}/conf.yaml"))
306         newconfig = YAML::load_file("#{@@bot.botclass}/conf.yaml")
307         @@config.update newconfig
308       else
309         # first-run wizard!
310         BotConfigWizard.new(@@bot).run
311         # save newly created config
312         save
313       end
314     end
315
316     # write current configuration to #{botclass}/conf.rbot
317     def save
318       File.open("#{@@bot.botclass}/conf.yaml", "w") do |file|
319         file.puts @@config.to_yaml
320       end
321     end
322
323     def privmsg(m)
324       @handler.handle(m)
325     end
326   end
327
328   class BotConfigWizard
329     def initialize(bot)
330       @bot = bot
331       @questions = BotConfig.items.values.find_all {|i| i.wizard }
332     end
333     
334     def run()
335       puts "First time rbot configuration wizard"
336       puts "===================================="
337       puts "This is the first time you have run rbot with a config directory of:"
338       puts @bot.botclass
339       puts "This wizard will ask you a few questions to get you started."
340       puts "The rest of rbot's configuration can be manipulated via IRC once"
341       puts "rbot is connected and you are auth'd."
342       puts "-----------------------------------"
343
344       return unless @questions
345       @questions.sort{|a,b| a.order <=> b.order }.each do |q|
346         puts q.desc
347         begin
348           print q.key + " [#{q.to_s}]: "
349           response = STDIN.gets
350           response.chop!
351           unless response.empty?
352             q.set_string response, false
353           end
354           puts "configured #{q.key} => #{q.to_s}"
355           puts "-----------------------------------"
356         rescue ArgumentError => e
357           puts "failed to set #{q.key}: #{e.message}"
358           retry
359         end
360       end
361     end
362   end
363 end