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