]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/config.rb
Sat Jul 30 01:19:32 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 BotConfigItem
7     attr_reader :type
8     attr_reader :desc
9     attr_reader :key
10     attr_reader :values
11     def initialize(key, params)
12       @key = key
13       if params.has_key? :default
14         @default = params[:default]
15       else
16         @default = false
17       end
18       @desc = params[:desc]
19       @type = params[:type] || String
20       @values = params[:values]
21       @on_change = params[:on_change]
22     end
23     def default
24       if @default.class == Proc
25         @default.call
26       else
27         @default
28       end
29     end
30     def on_change(newvalue)
31       return unless @on_change
32       @on_change.call(newvalue)
33     end
34   end
35
36   # container for bot configuration
37   class BotConfig
38     class Enum
39     end
40     class Password
41     end
42     class Boolean
43     end
44     
45     attr_reader :items
46     @@items = Hash.new
47     
48     def BotConfig.register(key, params)
49       unless params.nil? || params.instance_of?(Hash)
50         raise ArgumentError,"params must be a hash"
51       end
52       raise ArgumentError,"params must contain a period" unless key =~ /^.+\..+$/
53       @@items[key] = BotConfigItem.new(key, params)
54     end
55
56     # currently we store values in a hash but this could be changed in the
57     # future. We use hash semantics, however.
58     # components that register their config keys and setup defaults are
59     # supported via []
60     def [](key)
61       return @config[key] if @config.has_key?(key)
62       return @@items[key].default if @@items.has_key?(key)
63       return false
64     end
65     
66     # pass everything through to the hash
67     def method_missing(method, *args, &block)
68       return @config.send(method, *args, &block)
69     end
70
71     def handle_list(m, params)
72       modules = []
73       if params[:module]
74         @@items.each_key do |key|
75           mod, name = key.split('.')
76           next unless mod == params[:module]
77           modules.push name unless modules.include?(name)
78         end
79         if modules.empty?
80           m.reply "no such module #{params[:module]}"
81         else
82           m.reply "module #{params[:module]} contains: " + modules.join(", ")
83         end
84       else
85         @@items.each_key do |key|
86           name = key.split('.').first
87           modules.push name unless modules.include?(name)
88         end
89         m.reply "modules: " + modules.join(", ")
90       end
91     end
92
93     def handle_get(m, params)
94       key = params[:key]
95       unless @@items.has_key?(key)
96         m.reply "no such config key #{key}"
97       end
98       value = self[key]
99       if @@items[key].type == :array
100         value = self[key].join(", ")
101       elsif @@items[key].type == :password && !m.private
102         value = "******"
103       end
104       m.reply "#{key}: #{value}"
105     end
106
107     def handle_desc(m, params)
108       key = params[:key]
109       unless @@items.has_key?(key)
110         m.reply "no such config key #{key}"
111       end
112       m.reply "#{key}: #{@@items[key].desc}"
113     end
114
115     def handle_unset(m, params)
116       key = params[:key]
117       unless @@items.has_key?(key)
118         m.reply "no such config key #{key}"
119       end
120       @config.delete(key)
121       handle_get(m, params)
122     end
123
124     def handle_set(m, params)
125       key = params[:key]
126       value = params[:value].to_s
127       unless @@items.has_key?(key)
128         m.reply "no such config key #{key}"
129       end
130       item = @@items[key]
131       puts "item type is #{item.type}"
132       case item.type
133         when :string
134           @config[key] = value
135         when :password
136           @config[key] = value
137         when :integer
138           @config[key] = value.to_i
139         when :float
140           @config[key] = value.to_f
141         when :array
142           @config[key] = value.split(/,\s*/)
143         when :boolean
144           if value == "true"
145             @config[key] = true
146           else
147             @config[key] = false
148           end
149         when :enum
150           unless item.values.include?(value)
151             m.reply "invalid value #{value}, allowed values are: " + item.values.join(", ")
152             return
153           end
154           @config[key] = value
155         else
156           puts "ACK, unsupported type #{item.type}"
157           exit 2
158       end
159       item.on_change(@config[key])
160       m.okay
161     end
162
163     # bot:: parent bot class
164     # create a new config hash from #{botclass}/conf.rbot
165     def initialize(bot)
166       @bot = bot
167       @config = Hash.new(false)
168
169       # respond to config messages, to provide runtime configuration
170       # management
171       # messages will be:
172       #  get (implied)
173       #  set
174       #  unset
175       #  and for arrays:
176       #    add
177       #    remove
178       @handler = MessageMapper.new(self)
179       @handler.map 'config list :module', :action => 'handle_list',
180                    :defaults => {:module => false}
181       @handler.map 'config get :key', :action => 'handle_get'
182       @handler.map 'config desc :key', :action => 'handle_desc'
183       @handler.map 'config describe :key', :action => 'handle_desc'
184       @handler.map 'config set :key *value', :action => 'handle_set'
185       @handler.map 'config unset :key', :action => 'handle_unset'
186       
187       # TODO
188       # have this class persist key/values in hash using yaml as it kinda
189       # already does.
190       # have other users of the class describe config to it on init, like:
191       # @config.add(:key => 'server.name', :type => 'string',
192       #             :default => 'localhost', :restart => true,
193       #             :help => 'irc server to connect to')
194       # that way the config module doesn't have to know about all the other
195       # classes but can still provide help and defaults.
196       # Classes don't have to add keys, they can just use config as a
197       # persistent hash, but then they won't be presented by the config
198       # module for runtime display/changes.
199       # (:restart, if true, makes the bot reply to changes with "this change
200       # will take effect after the next restart)
201       #  :proc => Proc.new {|newvalue| ...}
202       # (:proc, proc to run on change of setting)
203       #  or maybe, @config.add_key(...) do |newvalue| .... end
204       #  :validate => /regex/
205       # (operates on received string before conversion)
206       # Special handling for arrays so the config module can be used to
207       # add/remove elements as well as changing the whole thing
208       # Allow config options to list possible valid values (if type is enum,
209       # for example). Then things like the language module can list the
210       # available languages for choosing.
211       
212       if(File.exist?("#{@bot.botclass}/conf.yaml"))
213         newconfig = YAML::load_file("#{@bot.botclass}/conf.yaml")
214         @config.update(newconfig)
215       else
216         # first-run wizard!
217         wiz = BotConfigWizard.new(@bot)
218         newconfig = wiz.run(@config)
219         @config.update(newconfig)
220       end
221     end
222
223     # write current configuration to #{botclass}/conf.rbot
224     def save
225       Dir.mkdir("#{@bot.botclass}") if(!File.exist?("#{@bot.botclass}"))
226       File.open("#{@bot.botclass}/conf.yaml", "w") do |file|
227         file.puts @config.to_yaml
228       end
229     end
230
231     def privmsg(m)
232       @handler.handle(m)
233     end
234   end
235
236   # I don't see a nice way to avoid the first start wizard knowing way too
237   # much about other modules etc, because it runs early and stuff it
238   # configures is used to initialise the other modules...
239   # To minimise this we'll do as little as possible and leave the rest to
240   # online modification
241   class BotConfigWizard
242
243     # TODO things to configure..
244     # config directory (botclass) - people don't realise they should set
245     # this. The default... isn't good.
246     # users? - default *!*@* to 10
247     # levels? - need a way to specify a default level, methinks, for
248     # unconfigured items.
249     #
250     def initialize(bot)
251       @bot = bot
252       @questions = [
253         {
254           :question => "What server should the bot connect to?",
255           :key => "server.name",
256           :type => :string,
257         },
258         {
259           :question => "What port should the bot connect to?",
260           :key => "server.port",
261           :type => :number,
262         },
263         {
264           :question => "Does this IRC server require a password for access? Leave blank if not.",
265           :key => "server.password",
266           :type => :password,
267         },
268         {
269           :question => "Would you like rbot to bind to a specific local host or IP? Leave blank if not.",
270           :key => "server.bindhost",
271           :type => :string,
272         },
273         {
274           :question => "What IRC nickname should the bot attempt to use?",
275           :key => "irc.nick",
276           :type => :string,
277         },
278         {
279           :question => "What local user should the bot appear to be?",
280           :key => "irc.user",
281           :type => :string,
282         },
283         {
284           :question => "What channels should the bot always join at startup? List multiple channels using commas to separate. If a channel requires a password, use a space after the channel name. e.g: '#chan1, #chan2, #secretchan secritpass, #chan3'",
285           :prompt => "Channels",
286           :key => "irc.join_channels",
287           :type => :string,
288         },
289         {
290           :question => "Which language file should the bot use?",
291           :key => "core.language",
292           :type => :enum,
293           :items => Dir.new(Config::DATADIR + "/languages").collect {|f|
294             f =~ /\.lang$/ ? f.gsub(/\.lang$/, "") : nil
295           }.compact
296         },
297         {
298           :question => "Enter your password for maxing your auth with the bot (used to associate new hostmasks with your owner-status etc)",
299           :key => "auth.password",
300           :type => :password,
301         },
302       ]
303     end
304     
305     def run(defaults)
306       config = defaults.clone
307       puts "First time rbot configuration wizard"
308       puts "===================================="
309       puts "This is the first time you have run rbot with a config directory of:"
310       puts @bot.botclass
311       puts "This wizard will ask you a few questions to get you started."
312       puts "The rest of rbot's configuration can be manipulated via IRC once"
313       puts "rbot is connected and you are auth'd."
314       puts "-----------------------------------"
315
316       @questions.each do |q|
317         puts q[:question]
318         begin
319           key = q[:key]
320           if q[:type] == :enum
321             puts "valid values are: " + q[:items].join(", ")
322           end
323           if (defaults.has_key?(key))
324             print q[:key] + " [#{defaults[key]}]: "
325           else
326             print q[:key] + " []: "
327           end
328           response = STDIN.gets
329           response.chop!
330           response = defaults[key] if response == "" && defaults.has_key?(key)
331           case q[:type]
332             when :string
333             when :number
334               raise "value '#{response}' is not a number" unless (response.class == Fixnum || response =~ /^\d+$/)
335               response = response.to_i
336             when :password
337             when :enum
338               raise "selected value '#{response}' is not one of the valid values" unless q[:items].include?(response)
339           end
340           config[key] = response
341           puts "configured #{key} => #{config[key]}"
342           puts "-----------------------------------"
343         rescue RuntimeError => e
344           puts e.message
345           retry
346         end
347       end
348       return config
349     end
350   end
351 end