summaryrefslogtreecommitdiff
path: root/lib/rbot/config.rb
blob: ef63a2feb06c36f1372e75d412d199814b633543 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
require 'singleton'

require 'yaml'

unless YAML.respond_to?(:load_file)
  def YAML.load_file( filepath )
    File.open( filepath ) do |f|
      YAML::load( f )
    end
  end
end


module Irc

class Bot
module Config
  class Value
    # allow the definition order to be preserved so that sorting by
    # definition order is possible. The Wizard does this to allow
    # the :wizard questions to be in a sensible order.
    @@order = 0
    attr_reader :type
    attr_reader :desc
    attr_reader :key
    attr_reader :wizard
    attr_reader :store_default
    attr_reader :requires_restart
    attr_reader :requires_rescan
    attr_reader :order
    attr_reader :manager
    attr_reader :auth_path
    def initialize(key, params)
      @manager = Config.manager
      # Keys must be in the form 'module.name'.
      # They will be internally passed around as symbols,
      # but we accept them both in string and symbol form.
      unless key.to_s =~ /^.+\..+$/
        raise ArgumentError,"key must be of the form 'module.name'"
      end
      @order = @@order
      @@order += 1
      @key = key.to_sym
      if @manager.overrides.key?(@key)
        @default = @manager.overrides[@key]
      elsif params.has_key? :default
        @default = params[:default]
      else
        @default = false
      end
      @desc = params[:desc]
      @type = params[:type] || String
      @on_change = params[:on_change]
      @validate = params[:validate]
      @wizard = params[:wizard]
      @store_default = params[:store_default]
      @requires_restart = params[:requires_restart]
      @requires_rescan = params[:requires_rescan]
      @auth_path = "config::key::#{key.sub('.','::')}"
    end
    def default
      if @default.instance_of?(Proc)
        @default.call
      else
        @default
      end
    end
    def get
      return @manager.config[@key] if @manager.config.has_key?(@key)
      return default
    end
    alias :value :get
    def set(value, on_change = true)
      @manager.config[@key] = value
      @manager.changed = true
      @on_change.call(@manager.bot, value) if on_change && @on_change
      return self
    end
    def unset
      @manager.config.delete(@key)
      @manager.changed = true
      @on_change.call(@manager.bot, value) if @on_change
      return self
    end

    # set string will raise ArgumentErrors on failed parse/validate
    def set_string(string, on_change = true)
      value = parse string
      if validate value
        set value, on_change
      else
        raise ArgumentError, "invalid value: #{string}"
      end
    end

    # override this. the default will work for strings only
    def parse(string)
      string
    end

    def to_s
      get.to_s
    end

    protected
    def validate(val, validator  = @validate)
      case validator
      when false, nil
        return true
      when Proc
        return validator.call(val)
      when Regexp
        raise ArgumentError, "validation via Regexp only supported for strings!" unless String === val
        return validator.match(val)
      else
        raise ArgumentError, "validation type #{validator.class} not supported"
      end
    end
  end

  class StringValue < Value
  end

  class BooleanValue < Value
    def parse(string)
      return true if string == "true"
      return false if string == "false"
      if string =~ /^-?\d+$/
        return string.to_i != 0
      end
      raise ArgumentError, "#{string} does not match either 'true' or 'false', and it's not an integer either"
    end
    def get
      r = super
      if r.kind_of?(Integer)
        return r != 0
      else
        return r
      end
    end
  end

  class IntegerValue < Value
    def parse(string)
      return 1 if string == "true"
      return 0 if string == "false"
      raise ArgumentError, "not an integer: #{string}" unless string =~ /^-?\d+$/
      string.to_i
    end
    def get
      r = super
      if r.kind_of?(Integer)
        return r
      else
        return r ? 1 : 0
      end
    end
  end

  class FloatValue < Value
    def parse(string)
      raise ArgumentError, "not a float #{string}" unless string =~ /^-?[\d.]+$/
      string.to_f
    end
  end

  class ArrayValue < Value
    def initialize(key, params)
      super
      @validate_item = params[:validate_item]
      @validate ||= Proc.new do |v|
        !v.find { |i| !validate_item(i) }
      end
    end

    def validate_item(item)
      validate(item, @validate_item)
    end

    def parse(string)
      string.split(/,\s+/)
    end
    def to_s
      get.join(", ")
    end
    def add(val)
      newval = self.get.dup
      unless newval.include? val
        newval << val
        validate_item(val) or raise ArgumentError, "invalid item: #{val}"
        validate(newval) or raise ArgumentError, "invalid value: #{newval.inspect}"
        set(newval)
      end
    end
    def rm(val)
      curval = self.get
      raise ArgumentError, "value #{val} not present" unless curval.include?(val)
      set(curval - [val])
    end
  end

  class EnumValue < Value
    def initialize(key, params)
      super
      @values = params[:values]
    end
    def values
      if @values.instance_of?(Proc)
        return @values.call(@manager.bot)
      else
        return @values
      end
    end
    def parse(string)
      unless values.include?(string)
        raise ArgumentError, "invalid value #{string}, allowed values are: " + values.join(", ")
      end
      string
    end
    def desc
      _("%{desc} [valid values are: %{values}]") % {:desc => @desc, :values => values.join(', ')}
    end
  end

  # container for bot configuration
  class ManagerClass

    include Singleton

    attr_reader :bot
    attr_reader :items
    attr_reader :config
    attr_reader :overrides
    attr_accessor :changed

    def initialize
      bot_associate(nil,true)
    end

    def reset_config
      @items = Hash.new
      @config = Hash.new(false)

      # We allow default values for config keys to be overridden by
      # the config file /etc/rbot.conf
      # The main purpose for this is to allow distro or system-wide
      # settings such as external program paths (figlet, toilet, ispell)
      # to be set once for all the bots.
      @overrides = Hash.new
      etcfile = "/etc/rbot.conf"
      if File.exist?(etcfile)
        log "Loading defaults from #{etcfile}"
        etcconf = YAML::load_file(etcfile)
        etcconf.each { |k, v|
          @overrides[k.to_sym] = v
        }
      end
    end

    # Associate with bot _bot_
    def bot_associate(bot, reset=false)
      reset_config if reset
      @bot = bot
      return unless @bot

      @changed = false
      conf = @bot.path 'conf.yaml'
      if File.exist? conf
        begin
          newconfig = YAML::load_file conf
          newconfig.each { |key, val|
            @config[key.to_sym] = val
          }
          return
        rescue
          error "failed to read conf.yaml: #{$!}"
        end
      end
      # config options with :store_default to true should store
      # their default value at first run.
      # Some defaults might change anytime the bot starts
      #  for instance core.db or authpw
      @items.values.find_all {|i| i.store_default }.each do |value|
        @config[value.key] = value.default
      end

      # if we got here, we need to run the first-run wizard
      Wizard.new(@bot).run
      # save newly created config
      @changed = true
      save
    end

    def register(item)
      unless item.kind_of?(Value)
        raise ArgumentError,"item must be an Irc::Bot::Config::Value"
      end
      @items[item.key] = item
    end

    # currently we store values in a hash but this could be changed in the
    # future. We use hash semantics, however.
    # components that register their config keys and setup defaults are
    # supported via []
    def [](key)
      # return @items[key].value if @items.has_key?(key)
      return @items[key.to_sym].value if @items.has_key?(key.to_sym)
      # try to still support unregistered lookups
      # but warn about them
      #      if @config.has_key?(key)
      #        warning "Unregistered lookup #{key.inspect}"
      #        return @config[key]
      #      end
      if @config.has_key?(key.to_sym)
        warning _("Unregistered lookup #{key.to_sym.inspect}")
        return @config[key.to_sym]
      end
      return false
    end

    def []=(key, value)
      return @items[key.to_sym].set(value) if @items.has_key?(key.to_sym)
      if @config.has_key?(key.to_sym)
        warning _("Unregistered lookup #{key.to_sym.inspect}")
        return @config[key.to_sym] = value
      end
    end

    # pass everything else through to the hash
    def method_missing(method, *args, &block)
      return @config.send(method, *args, &block)
    end

    # write current configuration to #{botclass}/conf.yaml
    def save
      if not @changed
        debug "Not writing conf.yaml (unchanged)"
        return
      end
      begin
	conf = @bot.path 'conf.yaml'
	fnew = conf + '.new'
        debug "Writing new conf.yaml ..."
        File.open(fnew, "w") do |file|
          savehash = {}
          @config.each { |key, val|
            savehash[key.to_s] = val
          }
          file.puts savehash.to_yaml
        end
        debug "Officializing conf.yaml ..."
        File.rename(fnew, conf)
        @changed = false
      rescue => e
        error "failed to write configuration file conf.yaml! #{$!}"
        error "#{e.class}: #{e}"
        error e.backtrace.join("\n")
      end
    end
  end

  # Returns the only Irc::Bot::Config::ManagerClass
  #
  def Config.manager
    return ManagerClass.instance
  end

  # Register a config value
  def Config.register(item)
    Config.manager.register(item)
  end

  class Wizard
    def initialize(bot)
      @bot = bot
      @manager = Config.manager
      @questions = @manager.items.values.find_all {|i| i.wizard }
    end

    def run()
      $stdout.sync = true
      puts _("First time rbot configuration wizard")
      puts "===================================="
      puts _("This is the first time you have run rbot with a config directory of: #{@bot.botclass}")
      puts _("This wizard will ask you a few questions to get you started.")
      puts _("The rest of rbot's configuration can be manipulated via IRC once rbot is connected and you are auth'd.")
      puts "-----------------------------------"

      return unless @questions
      @questions.sort{|a,b| a.order <=> b.order }.each do |q|
        puts _(q.desc)
        begin
          print q.key.to_s + " [#{q.to_s}]: "
          response = STDIN.gets
          response.chop!
          unless response.empty?
            q.set_string response, false
          end
          puts _("configured #{q.key} => #{q.to_s}")
          puts "-----------------------------------"
        rescue ArgumentError => e
          puts _("failed to set #{q.key}: #{e.message}")
          retry
        end
      end
    end
  end

end
end
end