butler 1.8.1 → 1.8.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. data/CHANGELOG.txt +212 -0
  2. data/{README → README.txt} +0 -0
  3. data/Rakefile +16 -11
  4. data/bin/botcontrol +35 -14
  5. data/data/butler/dialogs/create.rb +29 -40
  6. data/data/butler/dialogs/create_config.rb +1 -1
  7. data/data/butler/dialogs/dir.rb +13 -0
  8. data/data/butler/dialogs/en/create.yaml +24 -10
  9. data/data/butler/dialogs/en/dir.yaml +5 -0
  10. data/data/butler/dialogs/en/help.yaml +28 -11
  11. data/data/butler/dialogs/en/quickcreate.yaml +14 -0
  12. data/data/butler/dialogs/help.rb +16 -4
  13. data/data/butler/dialogs/quickcreate.rb +49 -0
  14. data/data/butler/plugins/core/access.rb +211 -0
  15. data/data/butler/plugins/core/logout.rb +11 -11
  16. data/data/butler/plugins/core/plugins.rb +23 -41
  17. data/data/butler/plugins/dev/bleakhouse.rb +46 -0
  18. data/data/butler/plugins/games/roll.rb +1 -1
  19. data/data/butler/plugins/operator/deop.rb +15 -20
  20. data/data/butler/plugins/operator/devoice.rb +14 -20
  21. data/data/butler/plugins/operator/limit.rb +56 -21
  22. data/data/butler/plugins/operator/op.rb +15 -20
  23. data/data/butler/plugins/operator/voice.rb +15 -20
  24. data/data/butler/plugins/service/define.rb +3 -3
  25. data/data/butler/plugins/service/more.rb +40 -0
  26. data/data/butler/plugins/util/cycle.rb +1 -1
  27. data/data/butler/plugins/util/load.rb +5 -5
  28. data/data/butler/plugins/util/pong.rb +3 -2
  29. data/lib/access/privilege.rb +17 -0
  30. data/lib/access/role.rb +33 -2
  31. data/lib/access/savable.rb +6 -0
  32. data/lib/access/yamlbase.rb +1 -2
  33. data/lib/butler/bot.rb +40 -7
  34. data/lib/butler/debuglog.rb +17 -0
  35. data/lib/butler/dialog.rb +1 -1
  36. data/lib/butler/irc/{channels.rb → channellist.rb} +2 -2
  37. data/lib/butler/irc/client.rb +60 -79
  38. data/lib/butler/irc/client/filter.rb +12 -0
  39. data/lib/butler/irc/client/listener.rb +55 -0
  40. data/lib/butler/irc/client/listenerlist.rb +69 -0
  41. data/lib/butler/irc/hostmask.rb +31 -16
  42. data/lib/butler/irc/message.rb +3 -3
  43. data/lib/butler/irc/parser.rb +2 -2
  44. data/lib/butler/irc/parser/rfc2812.rb +2 -6
  45. data/lib/butler/irc/socket.rb +12 -6
  46. data/lib/butler/irc/string.rb +4 -0
  47. data/lib/butler/irc/user.rb +0 -6
  48. data/lib/butler/irc/{users.rb → userlist.rb} +2 -2
  49. data/lib/butler/irc/whois.rb +6 -0
  50. data/lib/butler/plugin.rb +48 -14
  51. data/lib/butler/plugin/configproxy.rb +20 -0
  52. data/lib/butler/plugin/mapper.rb +308 -24
  53. data/lib/butler/plugin/matcher.rb +3 -1
  54. data/lib/butler/plugin/more.rb +65 -0
  55. data/lib/butler/plugin/onhandlers.rb +4 -4
  56. data/lib/butler/plugin/trigger.rb +4 -2
  57. data/lib/butler/plugins.rb +1 -1
  58. data/lib/butler/session.rb +11 -0
  59. data/lib/butler/version.rb +1 -1
  60. data/lib/cloptions.rb +1 -1
  61. data/lib/diagnostics.rb +20 -0
  62. data/lib/dialogline.rb +1 -1
  63. data/lib/durations.rb +19 -6
  64. data/lib/event.rb +8 -5
  65. data/lib/installer.rb +10 -3
  66. data/lib/ostructfixed.rb +11 -0
  67. data/lib/ruby/kernel/daemonize.rb +1 -2
  68. data/test/butler/plugin/mapper.rb +46 -0
  69. metadata +28 -11
  70. data/CHANGELOG +0 -44
  71. data/data/butler/plugins/core/privilege.rb +0 -103
@@ -6,27 +6,47 @@
6
6
 
7
7
 
8
8
 
9
+ require 'thread'
10
+
11
+
12
+
9
13
  class Butler
10
14
  class Plugin
11
15
  class ConfigProxy
12
16
  def initialize(config, base="")
13
17
  @config = config
14
18
  @base = base
19
+ @mutex = Mutex.new
20
+ end
21
+
22
+ # Synchronize access to this piece of config
23
+ def synchronize
24
+ @mutex.synchronize {
25
+ yield self
26
+ }
15
27
  end
16
28
 
29
+ # assign a config key, hands off to Configuration#[]= and prepends
30
+ # Plugin#base to the key
17
31
  def []=(key, value)
18
32
  @config[key.empty? ? @base : "#{@base}/#{key}"] = value
19
33
  end
20
34
 
35
+ # access a config key, hands off to Configuration#[] and prepends
36
+ # Plugin#base to the key
21
37
  def [](key, *args)
22
38
  @config[(key.empty? ? @base : "#{@base}/#{key}"), *args]
23
39
  end
24
40
 
41
+ # test existence of a key, hands off to Configuration#exist? and prepends
42
+ # Plugin#base to the key
25
43
  def exist?(key)
26
44
  @config.exist?(key.empty? ? @base : "#{@base}/#{key}")
27
45
  end
28
46
  alias has_key? exist?
29
47
 
48
+ # delete a config key, hands off to Configuration#delete and prepends
49
+ # Plugin#base to the key
30
50
  def delete(key)
31
51
  @config.delete(key.empty? ? @base : "#{@base}/#{key}")
32
52
  end
@@ -7,13 +7,94 @@
7
7
 
8
8
 
9
9
  require 'butler/plugin'
10
- require 'ostruct'
10
+ require 'ostructfixed'
11
11
 
12
12
 
13
13
 
14
14
  class Butler
15
15
  class Plugin
16
+ # For TypeMaps, to indicate that a validation failed
17
+ class ValidationFailure < RuntimeError; end
18
+
19
+ # Map arguments of a specific type to their actual value
20
+ # The mapping may raise an exception inherited from
21
+ # Butler::Plugin::ValidationFailure to indicate that the value
22
+ # didn't validate
23
+ class TypeMap
24
+
25
+ # name of the typemap
26
+ attr_reader :name
27
+
28
+ # type matches only this regex
29
+ attr_reader :regex
30
+
31
+ # validate and convert the matched value
32
+ attr_reader :validation
33
+
34
+ # Example:
35
+ # TypeMap.new "IP", /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/ do |butler, value|
36
+ # value.split(/\./).map { |e|
37
+ # i = e.to_i
38
+ # raise ValidationFailure unless i.between?(0,255)
39
+ # i
40
+ # }
41
+ # end
42
+ def initialize(name, regex, &validation)
43
+ @name = name
44
+ @regex = regex
45
+ @validation = validation
46
+ end
47
+
48
+ # map the value, can raise a ValidationFailure
49
+ def map(butler, value)
50
+ @validation ? @validation[butler, value] : value
51
+ end
52
+ end
53
+
16
54
  class Mapper
55
+ include Comparable
56
+
57
+ # priority of Mappers, don't change. Mappers should run with higher priority than
58
+ # less precise invocation mechanisms.
59
+ Priority = 10
60
+
61
+ module Pattern
62
+ # A portion that may contain color information
63
+ def self.colored(item)
64
+ "#{Color}?#{Regexp.escape(item)}#{Color}?"
65
+ end
66
+
67
+ # Multiple items of a specific regex
68
+ def self.one_or_more_of(single)
69
+ "(?:#{single}(?:(?:,\\s*|\\s+)#{single})*?)"
70
+ end
71
+
72
+ def self.one_of(items)
73
+ "(?i:#{items.map { |item| Regexp.escape(item) }.join('|')})"
74
+ end
75
+
76
+ def self.quoted_variants(pattern)
77
+ "(?:#{pattern}|\"#{pattern}\"|'#{pattern}')"
78
+ end
79
+
80
+ # MIRC color information
81
+ Color = '(?:[\x02\x0f\x12\x1f\x1d\x09]|\cc\d{1,2}(?:,\d{1,2})?)'.freeze
82
+
83
+ # A single argument
84
+ Argument = '(?:(?:\\\\.|[^\\\\"\'\s])+|"(?:\\\\.|[^\\\\"])*"|\'(?:\\\\.|[^\\\\\'])*\')'.freeze
85
+
86
+ # A list of arguments, comma or whitespace separated
87
+ Arguments = one_or_more_of(Argument).freeze
88
+
89
+ # An arbitrary string
90
+ String = "(?:.*?)".freeze
91
+
92
+ # In the definition string, a variable definition
93
+ Variable = /([+:*])?(\w+)(?:@([\w+-]+))?(?:\{([\w,]+)\})?|(\S+)/
94
+ end
95
+
96
+ Capture = Struct.new("Capture", :name, :scan, :typemap)
97
+
17
98
  attr_reader :authorization
18
99
  attr_reader :hash
19
100
  attr_reader :language
@@ -33,32 +114,55 @@ class Butler
33
114
  @name = "mapper:#{@language}:#{@expression}".freeze
34
115
  @hash = @name.hash
35
116
 
36
- tiles = expression.strip.scan(/(?:^|\s)[:*]\w+(?:\[[^\]]+\])?|\S+/).map { |word|
37
- word.strip!
38
- case word
39
- when /:(\w+)(.*)/
40
- @captures << [$1.to_sym, $2]
41
- '(\S+)'
42
- when /\*(\w+)(.*)/
43
- @captures << [$1.to_sym, $2]
44
- '(\S+(?:\s+\S+)*?)'
45
- else
46
- Regexp.escape(word)
117
+ # process expression
118
+ structure = structure(expression)
119
+ raise "No Trigger" unless String === structure.first
120
+ trigger, tmp = structure.shift.split(/\s+/, 2)
121
+ trigger.strip!
122
+ structure.unshift(tmp) if tmp and !tmp.empty?
123
+ raise "No Trigger" if (!trigger || trigger.empty? || trigger =~ /\W/)
124
+
125
+ append = "^"+Pattern.colored(trigger)
126
+ captures = []
127
+ structure.each { |item|
128
+ if Array === item
129
+ optional(item, append)
130
+ else
131
+ regexify(item, append)
47
132
  end
48
133
  }
49
- @regexp = Regexp.new('^'+tiles.join('\s+')+'$')
134
+ append << "$"
135
+
136
+ @regexp = Regexp.new(append)
50
137
  end
51
138
 
52
139
  def invoked_by?(message)
53
- if match = @regexp.match(message.post_arguments[0]) then
54
- params = {}
55
- @captures.zip(match.captures) { |(name, valid), value|
56
- params[name] = value
57
- }
58
- [OpenStruct.new(params)]
59
- else
60
- nil
61
- end
140
+ return nil unless match = @regexp.match(message.text[message.invocation.length..-1])
141
+ params = {}
142
+ butler = @plugin.butler
143
+ @captures.zip(match.captures) { |capture, value|
144
+ if value then
145
+ if capture.scan then
146
+ params[capture.name] = value.scan(capture.scan).map { |element|
147
+ if capture.typemap then
148
+ capture.typemap.map(butler, unwrap(element))
149
+ else
150
+ unwrap(element)
151
+ end
152
+ }
153
+ else
154
+ params[capture.name] = capture.typemap ? capture.typemap.map(butler, unwrap(value)) : unwrap(value)
155
+ end
156
+ else
157
+ params[capture.name] = nil
158
+ end
159
+ }
160
+ [OpenStruct.new(params)]
161
+ rescue ValidationFailure
162
+ nil
163
+ rescue Exception => e
164
+ @plugin.exception(e)
165
+ nil
62
166
  end
63
167
 
64
168
  def call(message, params)
@@ -66,11 +170,11 @@ class Butler
66
170
  end
67
171
 
68
172
  def priority
69
- -10
173
+ Priority
70
174
  end
71
175
 
72
176
  def <=>(other)
73
- -10 <=> other.priority
177
+ other.priority <=> Priority
74
178
  end
75
179
 
76
180
  def abort_invocations?
@@ -80,6 +184,186 @@ class Butler
80
184
  def eql?(other)
81
185
  other.kind_of?(Mapper) && @name.eql?(other.name)
82
186
  end
187
+
188
+ def inspect
189
+ "#<%s:0x%08x %s %s>" % [
190
+ self.class,
191
+ object_id << 1,
192
+ @expression.inspect,
193
+ @regexp.inspect
194
+ ]
195
+ end
196
+
197
+ def to_s
198
+ "#<%s:0x%08x %s>" % [
199
+ self.class,
200
+ object_id << 1,
201
+ @expression.inspect
202
+ ]
203
+ end
204
+
205
+ private
206
+ # parse the optional parts of a mapping and structure it as nested array
207
+ def structure(str)
208
+ curr = []
209
+ stack = [curr]
210
+ offset = 0
211
+ o,c = str.index("[", offset), str.index("]", offset)
212
+
213
+ while o or c
214
+ if o && c && o < c then
215
+ substr = str[offset...o].strip
216
+ curr << substr unless substr.empty?
217
+ curr << []
218
+ stack << curr.last
219
+ curr = stack.last
220
+ offset = o+1
221
+ elsif c then
222
+ substr = str[offset...c].strip
223
+ curr << substr unless substr.empty?
224
+ stack.pop
225
+ curr = stack.last
226
+ offset = c+1
227
+ else
228
+ raise "Invalid expression, Orphan ["
229
+ end
230
+ o,c = str.index("[", offset), str.index("]", offset)
231
+ end
232
+ curr << str[offset..-1].strip unless offset == str.length
233
+ curr
234
+ end # structure
235
+
236
+ # recursively create the regex for structured optional parts
237
+ def optional(item, append)
238
+ append << "(?:"
239
+ item.each { |item|
240
+ if Array === item then
241
+ optional(item, append)
242
+ else
243
+ regexify(item, append)
244
+ end
245
+ }
246
+ append << ")??"
247
+ end
248
+
249
+ # convert a part into its regex pendant, parse out captures, types and restrictions
250
+ def regexify(string, append)
251
+ string.scan(Pattern::Variable) { |type, name, map, one_of, literal|
252
+ if literal then
253
+ # non-whitespace, non-word, non-argument(s) - probably interpunctuation
254
+ append << Regexp.escape(literal)
255
+ else
256
+ case type
257
+ when nil
258
+ raise "Forgot :, * or + prefix? Invalid pattern" if map or one_of
259
+ # literal
260
+ append << '\s+'+Pattern.colored(name)
261
+ when "+"
262
+ # string
263
+ append << "\\s+(#{Pattern::String})"
264
+ @captures << Capture.new(name.to_sym, nil, nil)
265
+ when ":"
266
+ # argument
267
+ raise "Unknown type '#{map}'" if map and !(typemap = @plugin.typemap(map))
268
+ if one_of then
269
+ append << "\\s+(#{Pattern.one_of(one_of.split(/,\s*/))})"
270
+ elsif map then
271
+ append << "\\s+(#{typemap.regex})"
272
+ else
273
+ append << "\\s+(#{Pattern::Argument})"
274
+ end
275
+ @captures << Capture.new(name.to_sym, nil, @plugin.typemap(map))
276
+ when "*"
277
+ # arguments
278
+ raise "Unknown type '#{map}'" if map and !(typemap = @plugin.typemap(map))
279
+ if one_of then
280
+ scan = Pattern.one_of(one_of.split(/,\s*/))
281
+ append << "\\s+(#{Pattern.one_or_more_of(scan)})"
282
+ scan = /#{scan}/
283
+ elsif map then
284
+ append << "\\s+(#{Pattern.one_or_more_of(typemap.regex)})"
285
+ scan = typemap.regex
286
+ else
287
+ append << "\\s+(#{Pattern.one_or_more_of(Pattern::Argument)})"
288
+ scan = /#{Pattern::Argument}/
289
+ end
290
+ @captures << Capture.new(name.to_sym, /#{scan}/, typemap)
291
+ end
292
+ end
293
+ }
294
+ end
295
+
296
+ # remove quotes if necessary and unescape
297
+ def unwrap(value)
298
+ value = case value[0,1]
299
+ when '"': value[1..-2].gsub(/\\./) { |m| ['"', "'", ' '].include?(m) ? m[1,1] : m }
300
+ when "'": value[1..-2].gsub(/\\./) { |m| ['"', "'", ' '].include?(m) ? m[1,1] : m }
301
+ else value.gsub(/\\./) { |m| ['"', "'", ' '].include?(m) ? m[1,1] : m }
302
+ end
303
+ end
304
+ end
305
+
306
+
307
+ # add a mapping type for :map:'s
308
+ # The regular expression MUST NOT contain any captures!
309
+ # For grouping use (?: ... ), which does not capture
310
+ # The provided block can convert the value and do additional testing for validity
311
+ # If the value is invalid, raise a ValidationFailure
312
+ # Example:
313
+ # map_type "IP", /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/ do |butler, value|
314
+ # value.split(/\./).map { |e|
315
+ # i = e.to_i
316
+ # raise ValidationFailure unless i.between?(0,255)
317
+ # i
318
+ # }
319
+ # end
320
+ def self.map_type(name, regex, &validation)
321
+ @mapping_type[name] = TypeMap.new(name, regex, &validation)
322
+ end
323
+
324
+ # get the TypeMap for a type
325
+ def self.typemap(name)
326
+ (@mapping_type || MappingTypes)[name]
327
+ end
328
+
329
+ # Basic mapping types, includes:
330
+ # * Integer: any valid integer. Converted to a Fix- or Bignum.
331
+ # * +Integer: any positive integer. Converted to a Fix- or Bignum.
332
+ # * -Integer: any negative integer. Converted to a Fix- or Bignum.
333
+ # * Float: any valid float. Converted to a Float.
334
+ # * +Float: any positive float. Converted to a Float.
335
+ # * -Float: any negative float. Converted to a Float.
336
+ # * Nick: a valid nickname (not necessarily online)
337
+ # * User: a currently online and for butler visible user. Converted to an IRC::User.
338
+ # * Channelname: a valid channelname (not necessarily one butler participates)
339
+ # * Channel: a channel butler is in. Converted to an IRC::Channel.
340
+ MappingTypes = {}
341
+ # Provide additional warnings
342
+ def MappingTypes.[]=(name, typemap) # :nodoc:
343
+ warn "redefining TypeMap '#{name}'" if has_key?(name)
344
+ super
83
345
  end
346
+ [
347
+ ["Integer", %r{[+-]?\d+}, proc { |butler, value| Integer(value) }],
348
+ ["+Integer", %r{\+?\d+}, proc { |butler, value| Integer(value) }],
349
+ ["-Integer", %r{\-\d+}, proc { |butler, value| Integer(value) }],
350
+ ["Float", %r{[+-]?\d+(?:\.\d+)}, proc { |butler, value| Float(value) }],
351
+ ["+Float", %r{\+?\d+(?:\.\d+)}, proc { |butler, value| Float(value) }],
352
+ ["-Float", %r{\-?\d+(?:\.\d+)}, proc { |butler, value| Float(value) }],
353
+ ["Nick", %r{[0-9A-Za-z_\-\|\\\[\]\{\}\^\`]+}],
354
+ ["Channelname", %r{[&#!\+][^\x07\x0A\x0D,: ]{1,50}}],
355
+ ["User", %r{[0-9A-Za-z_\-\|\\\[\]\{\}\^\`]+}, proc { |butler, value|
356
+ butler.users[value]
357
+ }],
358
+ ["Channel", %r{[&#!\+][^\x07\x0A\x0D,: ]{1,50}}, proc { |butler, value|
359
+ butler.channels[value]
360
+ }],
361
+ ].each { |name, regex, validation|
362
+ MappingTypes[name] = TypeMap.new(
363
+ name,
364
+ Mapper::Pattern.quoted_variants(regex),
365
+ &validation
366
+ )
367
+ }
84
368
  end
85
369
  end
@@ -9,6 +9,8 @@
9
9
  class Butler
10
10
  class Plugin
11
11
  class Matcher
12
+ include Comparable
13
+
12
14
  attr_reader :priority
13
15
  attr_reader :hash
14
16
  attr_reader :en_match
@@ -32,7 +34,7 @@ class Butler
32
34
  end
33
35
 
34
36
  def <=>(other)
35
- @priority <=> other.priority
37
+ other.priority <=> @priority
36
38
  end
37
39
 
38
40
  def eql?(other)
@@ -0,0 +1,65 @@
1
+ #--
2
+ # Copyright 2007 by Stefan Rusterholz.
3
+ # All rights reserved.
4
+ # See LICENSE.txt for permissions.
5
+ #++
6
+
7
+
8
+
9
+ class Butler
10
+ class Plugin
11
+ class More
12
+ MessageLength = 300
13
+ String = {
14
+ "en" => "![c(white),(black)]more...![o]".mirc_formatted,
15
+ "de" => "![c(white),(black)]mehr...![o]".mirc_formatted,
16
+ }
17
+
18
+ attr_reader :lead
19
+ attr_reader :text
20
+ attr_reader :index
21
+ attr_reader :length
22
+
23
+ def initialize(message, lead, text)
24
+ @language = message.language
25
+ @lead = !lead || lead.empty? ? nil : lead
26
+ @text = text
27
+ @index = 0
28
+ chunks = MessageLength-(lead ? lead.length+4 : 2)-tail.length
29
+ @pieces = text.scan(/.{1,#{chunks}}(?:\b|$)/m)
30
+ @length = @pieces.length
31
+ end
32
+
33
+ def succ
34
+ raise "Reached end already" if @index+1 >= @length
35
+ @pieces[@index+=1]
36
+ end
37
+
38
+ def prev
39
+ raise "Reached start already" if @index.zero?
40
+ @pieces[@index-=1]
41
+ end
42
+
43
+ def succ?
44
+ @index+1 < @length
45
+ end
46
+
47
+ def prev?
48
+ !@index.zero?
49
+ end
50
+
51
+ def current
52
+ @pieces[@index]
53
+ end
54
+
55
+ # show adds lead and tail depending on requirement (no tail for last message, no lead if empty)
56
+ def show
57
+ "#{lead+': ' if lead}#{@pieces[@index]}#{', '+tail if succ?}"
58
+ end
59
+
60
+ def tail
61
+ String[@language] || String["en"]
62
+ end
63
+ end
64
+ end
65
+ end