butler 1.8.1 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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