butler 1.8.0 → 1.8.1
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.
- data/CHANGELOG +40 -0
- data/README +9 -9
- data/Rakefile +15 -71
- data/bin/botcontrol +151 -146
- data/data/butler/dialogs/botcontrol.rb +8 -3
- data/data/butler/dialogs/create.rb +18 -18
- data/data/butler/dialogs/create_config.rb +8 -0
- data/data/butler/dialogs/en/create_config.yaml +2 -0
- data/data/butler/dialogs/en/list.yaml +2 -1
- data/data/butler/dialogs/info.rb +2 -2
- data/data/butler/dialogs/list.rb +13 -8
- data/data/butler/plugins/games/countdown.rb +41 -0
- data/data/butler/plugins/games/roll.rb +59 -0
- data/lib/access.rb +6 -107
- data/lib/access/admin.rb +3 -0
- data/lib/access/role.rb +37 -2
- data/lib/access/savable.rb +5 -0
- data/lib/access/user.rb +21 -2
- data/lib/access/yamlbase.rb +4 -0
- data/lib/butler.rb +4 -4
- data/lib/butler/bot.rb +13 -13
- data/lib/butler/irc/client.rb +10 -2
- data/lib/butler/irc/parser.rb +7 -2
- data/lib/butler/irc/parser/commands.rb +24 -7
- data/lib/butler/irc/parser/generic.rb +27 -315
- data/lib/butler/irc/parser/rfc2812.rb +328 -0
- data/lib/butler/irc/socket.rb +1 -1
- data/lib/butler/irc/user.rb +13 -0
- data/lib/butler/plugin.rb +1 -1
- data/lib/butler/plugin/configproxy.rb +4 -4
- data/lib/butler/plugins.rb +1 -1
- data/lib/butler/version.rb +1 -1
- data/lib/configuration.rb +22 -71
- data/lib/dialogline.rb +12 -0
- data/lib/event.rb +5 -2
- data/lib/installer.rb +52 -24
- data/lib/iterator.rb +17 -7
- data/lib/log.rb +32 -5
- data/lib/log/comfort.rb +33 -16
- data/lib/log/entry.rb +25 -5
- data/lib/log/fakeio.rb +1 -0
- data/lib/log/splitter.rb +6 -2
- data/lib/ostructfixed.rb +5 -0
- data/lib/ruby/exception/detailed.rb +3 -3
- data/lib/scheduler.rb +19 -4
- data/lib/scriptfile.rb +9 -2
- data/lib/string.rb +176 -0
- data/lib/string/ascii.rb +31 -0
- data/lib/string/mbencoded.rb +79 -0
- data/lib/string/sbencoded.rb +77 -0
- data/lib/string/utf8.rb +157 -0
- data/lib/templater.rb +68 -10
- data/lib/w3validator.rb +86 -0
- data/test/irc/serverlistings/test_rpl_hiddenhost.txt +60 -0
- data/test/test_access.rb +101 -0
- data/test/test_configuration.rb +63 -0
- metadata +19 -2
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'string'
|
2
|
+
|
3
|
+
# SingleByte encoded Strings, more speed than MBEncoded for operations like
|
4
|
+
# length, index, [] and []= (predefined length of characters)
|
5
|
+
class String::SBEncoded < String
|
6
|
+
def self.new(string, encoding, collation=nil)
|
7
|
+
raise "No encoding given" unless Encodings[encoding]
|
8
|
+
obj = allocate
|
9
|
+
obj.send(:initialize, string, encoding, collation)
|
10
|
+
obj
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :encoding
|
14
|
+
|
15
|
+
attr_reader :collation
|
16
|
+
|
17
|
+
def initialize(string, encoding, collation=nil)
|
18
|
+
super(string)
|
19
|
+
@encoding = encoding.freeze
|
20
|
+
@collation = collation.freeze
|
21
|
+
end
|
22
|
+
|
23
|
+
def <=>(other)
|
24
|
+
unless other.collation == @collation
|
25
|
+
raise ArgumentError, "Collations don't match: #{@collation} <=> #{other.collation}"
|
26
|
+
end
|
27
|
+
unless other.encoding == @encoding
|
28
|
+
raise ArgumentError.new("Encodings don't match: #{@encoding} <=> #{other.encoding}")
|
29
|
+
end
|
30
|
+
self.utf8 <=> other.utf8
|
31
|
+
end
|
32
|
+
|
33
|
+
# all methods delegated to utf8-ified strings (and if necessary it's reconversion)
|
34
|
+
def upcase; utf8.upcase.to_s(@encoding, @collation); end
|
35
|
+
def downcase; utf8.downcase.to_s(@encoding, @collation); end
|
36
|
+
def capitalize; utf8.capitalize.to_s(@encoding, @collation); end
|
37
|
+
|
38
|
+
def [](*args)
|
39
|
+
String::SBEncoded.new(super, @encoding, @collation)
|
40
|
+
end
|
41
|
+
alias slice []
|
42
|
+
|
43
|
+
def chop
|
44
|
+
String::SBEncoded.new(super, @encoding, @collation)
|
45
|
+
end
|
46
|
+
def strip
|
47
|
+
String::SBEncoded.new(super, @encoding, @collation)
|
48
|
+
end
|
49
|
+
def lstrip
|
50
|
+
String::SBEncoded.new(super, @encoding, @collation)
|
51
|
+
end
|
52
|
+
def rstrip
|
53
|
+
String::SBEncoded.new(super, @encoding, @collation)
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
# ! variants, using self.replace
|
58
|
+
def upcase!; replace(upcase); end
|
59
|
+
def downcase!; replace(downcase); end
|
60
|
+
def capitalize!; replace(capitalize); end
|
61
|
+
|
62
|
+
def dup
|
63
|
+
String::SBEncoded.new(self, @encoding, @collation)
|
64
|
+
end
|
65
|
+
|
66
|
+
def utf8(collation=nil)
|
67
|
+
String::UTF8.new(
|
68
|
+
Iconv.iconv(String::UTF8::UTF8, @encoding, self).first,
|
69
|
+
String::UTF8::UTF8,
|
70
|
+
collation || @collation
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
def inspect
|
75
|
+
"#{encoding}(#{collation||'none'}):#{super}"
|
76
|
+
end
|
77
|
+
end
|
data/lib/string/utf8.rb
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'string'
|
2
|
+
|
3
|
+
class String::UTF8 < String
|
4
|
+
unless method_defined?(:byte_insert)
|
5
|
+
UTF8 = 'utf-8'
|
6
|
+
alias byte_insert insert
|
7
|
+
alias original_succ! succ!
|
8
|
+
alias original_succ succ
|
9
|
+
private :original_succ!
|
10
|
+
private :original_succ
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.new(string, encoding, collation=nil)
|
14
|
+
raise "Encoding must be 'utf-8' but is '#{encoding}'" unless encoding == UTF8
|
15
|
+
obj = allocate
|
16
|
+
obj.send(:initialize, string, collation)
|
17
|
+
obj
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :collation
|
21
|
+
|
22
|
+
def initialize(string, collation=nil)
|
23
|
+
super(Unicode::normalize_KC(string))
|
24
|
+
@collation = collation
|
25
|
+
end
|
26
|
+
|
27
|
+
def encoding
|
28
|
+
String::UTF8::UTF8
|
29
|
+
end
|
30
|
+
|
31
|
+
# See String#[].
|
32
|
+
# May return an integer > 255 when used like "\342\210\205"[0] # => 8709
|
33
|
+
def [](arg1, arg2=nil) #:nodoc:
|
34
|
+
if arg2 then
|
35
|
+
unpack("U*").slice(arg1, arg2).pack("U*").utf8
|
36
|
+
elsif Range === arg1 then
|
37
|
+
unpack("U*").slice(arg1).pack("U*").utf8
|
38
|
+
else
|
39
|
+
unpack("U*").slice(*args)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
alias slice []
|
43
|
+
|
44
|
+
def []=(*args)
|
45
|
+
value = args.pop
|
46
|
+
codepoints = unpack("U*")
|
47
|
+
codepoints[*args] = value.utf8.unpack("U*")
|
48
|
+
replace(codepoints.pack("U*"))
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
def ==(other)
|
53
|
+
super(other.utf8)
|
54
|
+
end
|
55
|
+
|
56
|
+
def <=>(other)
|
57
|
+
raise "Can't compare strings with different collation" unless @collation == other.collation
|
58
|
+
Unicode.strcmp(self, other)
|
59
|
+
end
|
60
|
+
|
61
|
+
def length; @length||=unpack("U*").size; end
|
62
|
+
def upcase; Unicode::upcase(self).utf8; end
|
63
|
+
def downcase; Unicode::downcase(self).utf8; end
|
64
|
+
def capitalize; Unicode::capitalize(self).utf8; end
|
65
|
+
def swapcase
|
66
|
+
up = Unicode::upcase(self)
|
67
|
+
down = Unicode::downcase(self)
|
68
|
+
unpack("U*").zip(up.unpack("U*"), down.unpack("U*")).map { |n,u,d|
|
69
|
+
n == u ? d : u
|
70
|
+
}.pack("U*")
|
71
|
+
end
|
72
|
+
def reverse; unpack("U*").reverse.pack("U*").utf8; end
|
73
|
+
def strip; gsub(UNICODE_L_PAT, '').gsub(UNICODE_L_PAT, '').utf8; end
|
74
|
+
def lstrip; gsub(UNICODE_L_PAT, '').utf8; end
|
75
|
+
def rstrip; gsub(UNICODE_T_PAT, '').utf8; end
|
76
|
+
|
77
|
+
def index(item, offset=0)
|
78
|
+
case item
|
79
|
+
when Regexp
|
80
|
+
mb = unpack("U*")[offset..-1].pack("U*")
|
81
|
+
bi = mb.byte_index(item)
|
82
|
+
bi && mb.byte_slice(0,bi).unpack("U*").size+offset
|
83
|
+
when Integer
|
84
|
+
# sucks, but Array#index does not accept an offset-arg
|
85
|
+
unpack("U*")[offset..-1].index(item)+offset
|
86
|
+
else
|
87
|
+
raise "Must be of same encoding" if String === item and encoding != item.encoding
|
88
|
+
if offset.zero? then
|
89
|
+
bi = byte_index(item)
|
90
|
+
bi && byte_slice(0,bi).unpack("U*").size
|
91
|
+
else
|
92
|
+
index(Regexp.new(Regexp.escape(item)), offset)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def rindex(item, offset=-1)
|
98
|
+
case item
|
99
|
+
when Integer
|
100
|
+
unpack("U*")[0..offset].rindex(item)
|
101
|
+
else
|
102
|
+
raise "Must be of same encoding" if String === item and encoding != item.encoding
|
103
|
+
bi = byte_rindex(item, offset)
|
104
|
+
bi && byte_slice(0,bi).unpack("U*").size
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Inserts the string at codepoint offset specified in offset.
|
109
|
+
def insert(offset, fragment) #:nodoc:
|
110
|
+
replace(unpack("U*").insert(offset, fragment.unpack("U*")).flatten.pack("U*"))
|
111
|
+
end
|
112
|
+
|
113
|
+
def chop!
|
114
|
+
gsub!(/(?:.|\r?\n)\z/u, '')
|
115
|
+
end
|
116
|
+
|
117
|
+
def chop
|
118
|
+
gsub(/(?:.|\r?\n)\z/u, '')
|
119
|
+
end
|
120
|
+
|
121
|
+
def each_char(&block)
|
122
|
+
scan(/./um, &block)
|
123
|
+
self
|
124
|
+
end
|
125
|
+
|
126
|
+
def utf8(collation=nil)
|
127
|
+
String::UTF8.new(self, String::UTF8::UTF8, collation)
|
128
|
+
end
|
129
|
+
|
130
|
+
def dup
|
131
|
+
String::UTF8.new(self, String::UTF8::UTF8, @collation)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Decomposes the string and returns the decomposed string
|
135
|
+
def decompose #:nodoc:
|
136
|
+
Unicode::decompose(self)
|
137
|
+
end
|
138
|
+
|
139
|
+
# Normalizes the string to form C and returns the result
|
140
|
+
def normalize_C #:nodoc:
|
141
|
+
Unicode::normalize_C(self)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Normalizes the string to form D and returns the result
|
145
|
+
def normalize_D #:nodoc:
|
146
|
+
Unicode::normalize_D(self)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Normalizes the string to form KC and returns the result
|
150
|
+
def normalize_KC #:nodoc:
|
151
|
+
Unicode::normalize_KC(self)
|
152
|
+
end
|
153
|
+
|
154
|
+
def inspect
|
155
|
+
"#{encoding}(#{collation||'none'}):#{super}"
|
156
|
+
end
|
157
|
+
end
|
data/lib/templater.rb
CHANGED
@@ -11,22 +11,66 @@ require 'ostruct'
|
|
11
11
|
|
12
12
|
|
13
13
|
|
14
|
+
# = Indexing
|
15
|
+
# Author: Stefan Rusterholz
|
16
|
+
# Contact: apeiros@gmx.net>
|
17
|
+
# Version: 0.0.1
|
18
|
+
# Date: 2007-10-12
|
19
|
+
#
|
20
|
+
# = About
|
21
|
+
# A helper class for ERB, allows constructs like the one in the Synopsis to
|
22
|
+
# enable simple use of variables/methods in templates.
|
23
|
+
#
|
24
|
+
# = Synopsis
|
25
|
+
# tmpl = Templater.new("Hello <%= name %>!")
|
26
|
+
# tmpl.result(self, :name => 'world') # => 'Hello World!'
|
27
|
+
#
|
14
28
|
class Templater
|
29
|
+
module VERSION
|
30
|
+
STRING = "0.0.2"
|
31
|
+
end
|
32
|
+
|
33
|
+
# A proc for &on_error in Templater::Variables.new or Templater#result.
|
34
|
+
# Raises the error further on.
|
15
35
|
Raiser = proc { |e|
|
16
36
|
raise
|
17
37
|
}
|
38
|
+
|
39
|
+
# A proc for &on_error in Templater::Variables.new or Templater#result.
|
40
|
+
# Inserts <<error_class: error_message>> in the place where the error
|
41
|
+
# occurred.
|
18
42
|
Teller = proc { |e|
|
19
43
|
"<<#{e.class}: #{e}>>"
|
20
44
|
}
|
21
45
|
|
46
|
+
# = Indexing
|
47
|
+
# Author: Stefan Rusterholz
|
48
|
+
# Contact: apeiros@gmx.net>
|
49
|
+
# Version: 0.0.1
|
50
|
+
# Date: 2007-10-12
|
51
|
+
#
|
52
|
+
# = About
|
53
|
+
# Used by Templater to resolve variables.
|
54
|
+
#
|
55
|
+
# = Synopsis
|
56
|
+
# tmpl = Variables.new(delegator, :variable => "content") { |exception|
|
57
|
+
# do_something_with_exception
|
58
|
+
# }
|
59
|
+
#
|
22
60
|
class Variables < OpenStruct
|
61
|
+
|
62
|
+
# === Arguments
|
63
|
+
# * delegator: All method calls and undefined variables are delegated to this object as method call.
|
64
|
+
# * variables: A hash with variables in it, keys must be Symbols.
|
65
|
+
# * &on_error: The block is yielded in case of an exception with the exception as argument
|
66
|
+
#
|
23
67
|
def initialize(delegator=nil, variables={}, &on_error)
|
24
68
|
super(variables)
|
25
69
|
@delegator = delegator
|
26
70
|
@on_error = on_error || RaiseExceptions
|
27
71
|
end
|
28
72
|
|
29
|
-
def method_missing(m, *a, &b)
|
73
|
+
def method_missing(m, *a, &b) # :nodoc:
|
30
74
|
if @table.key?(m) || m.to_s =~ /=$/ then
|
31
75
|
super
|
32
76
|
elsif @delegator && @delegator.respond_to?(m) then
|
@@ -38,7 +82,7 @@ class Templater
|
|
38
82
|
@on_error.call(e)
|
39
83
|
end
|
40
84
|
|
41
|
-
def inspect
|
85
|
+
def inspect # :nodoc:
|
42
86
|
@table.inspect
|
43
87
|
end
|
44
88
|
end
|
@@ -49,24 +93,38 @@ class Templater
|
|
49
93
|
:trim_mode => '%<>',
|
50
94
|
:eoutvar => '_erbout'
|
51
95
|
}
|
96
|
+
|
52
97
|
# binding method
|
53
98
|
Binder = Object.instance_method(:binding)
|
54
99
|
|
100
|
+
# The template string
|
55
101
|
attr_reader :string
|
102
|
+
|
103
|
+
# Like Templater.new, but instead of a template string, the path to the file
|
104
|
+
# containing the template. Sets :filename.
|
105
|
+
def self.file(path, opt=nil)
|
106
|
+
new(File.read, (opt || {}).merge(:filename => path))
|
107
|
+
end
|
108
|
+
|
109
|
+
# ==== Arguments
|
110
|
+
# * string: The template string, it becomes frozen
|
111
|
+
# * opt: Option hash, keys:
|
112
|
+
# * :filename: The filename used for the evaluation (useful for error messages)
|
113
|
+
# * :safe_level: see ERB.new
|
114
|
+
# * :trim_mode: see ERB.new
|
115
|
+
# * :eoutvar: see ERB.new
|
56
116
|
def initialize(string, opt={})
|
57
117
|
opt, string = string, nil if string.kind_of?(Hash)
|
58
118
|
opt = Opt.merge(opt)
|
59
|
-
file =
|
60
|
-
|
61
|
-
string ||= opt[:filename]
|
62
|
-
file = string
|
63
|
-
string = File.read(string)
|
64
|
-
end
|
65
|
-
@string = string.dup.freeze
|
119
|
+
file = opt.delete(:filename)
|
120
|
+
@string = string.freeze
|
66
121
|
@erb = ERB.new(@string, *opt.values_at(:safe_level, :trim_mode, :eoutvar))
|
67
122
|
@erb.filename = file if file
|
68
123
|
end
|
69
124
|
|
125
|
+
# See Templater::Variables.new
|
126
|
+
# Returns the evaluated template. Default &on_error is the Templater::Raiser
|
127
|
+
# proc.
|
70
128
|
def result(delegator=nil, variables={}, &on_error)
|
71
129
|
variables ||= {}
|
72
130
|
on_error ||= Raiser
|
@@ -75,7 +133,7 @@ class Templater
|
|
75
133
|
rescue NameError => e
|
76
134
|
raise NameError, e.message+" for #{self.inspect} with #{variables.inspect}", e.backtrace
|
77
135
|
end
|
78
|
-
|
136
|
+
|
79
137
|
def inspect # :nodoc:
|
80
138
|
"#<%s:0x%x string=%s>" % [
|
81
139
|
self.class,
|
data/lib/w3validator.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'mechanize'
|
2
|
+
|
3
|
+
class W3Validator
|
4
|
+
attr_reader :glob
|
5
|
+
attr_reader :files
|
6
|
+
|
7
|
+
def initialize(glob, cache=nil)
|
8
|
+
@glob = glob.freeze
|
9
|
+
@files = glob ? Dir.glob(glob).freeze : [].freeze
|
10
|
+
@agent = WWW::Mechanize.new
|
11
|
+
# bug in current implementation? unpredictably raises EOF exceptions without this
|
12
|
+
@agent.keep_alive = false
|
13
|
+
@cache = cache ? Marshal.load(File.read(cache)) : nil
|
14
|
+
@cache_file = cache
|
15
|
+
require 'zlib' if @cache
|
16
|
+
end
|
17
|
+
|
18
|
+
def cache
|
19
|
+
data = {}
|
20
|
+
@files.each { |file| data[file] = Zlib.crc32(File.read(file)) }
|
21
|
+
File.open(cache, "w") { |fh| fh.write(Marshal.dump(data)) }
|
22
|
+
@cache = data
|
23
|
+
end
|
24
|
+
|
25
|
+
def cache_file(file)
|
26
|
+
require 'zlib'
|
27
|
+
data = Marshal.load(File.read(cache))
|
28
|
+
data[file] = Zlib.crc32(File.read(file))
|
29
|
+
File.open(cache, "w") { |fh| fh.write(Marshal.dump(data)) }
|
30
|
+
data
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate_changed(cachefile, delay=2)
|
34
|
+
require 'zlib'
|
35
|
+
crcs = File.exist?(cachefile) ? Marshal.load(File.read(cachefile)) {}
|
36
|
+
warn "No cachefile." unless File.exist?(cachefile)
|
37
|
+
orig_files = @files.dup
|
38
|
+
@files = @files.reject { |file| Zlib.crc32(File.read(file)) == crcs[file] }
|
39
|
+
puts "Validating files:", *@files
|
40
|
+
result = validate(delay, &block)
|
41
|
+
@files = orig_files.freeze
|
42
|
+
result
|
43
|
+
end
|
44
|
+
|
45
|
+
# returns whether all are valid, yields file, valid if a block was given.
|
46
|
+
def validate(delay=2)
|
47
|
+
@files.map { |file|
|
48
|
+
valid = valid?(file)
|
49
|
+
yield(file, valid) if block_given?
|
50
|
+
sleep(delay)
|
51
|
+
valid
|
52
|
+
}.all?
|
53
|
+
end
|
54
|
+
|
55
|
+
def rehash
|
56
|
+
@files = @glob ? Dir.glob(@glob).freeze : [].freeze
|
57
|
+
end
|
58
|
+
|
59
|
+
def glob=(glob)
|
60
|
+
@glob = glob
|
61
|
+
rehash
|
62
|
+
end
|
63
|
+
|
64
|
+
def valid?(file, tries=3, delay=3)
|
65
|
+
rescued = 0
|
66
|
+
form = @agent.get('http://validator.w3.org/').forms[1]
|
67
|
+
form.file_uploads.first.file_name = file
|
68
|
+
result = form.submit
|
69
|
+
cache_file(file) if @cache_file
|
70
|
+
case (result/"title").first.inner_text[/\[([^\/]+)\]/, 1]
|
71
|
+
when "Valid": true
|
72
|
+
when "Invalid": false
|
73
|
+
else raise "Title #{(result/"title").first.inner_text.gsub(/\s+/m, ' ')} not recognized"
|
74
|
+
end
|
75
|
+
rescue Exception => e
|
76
|
+
warn e
|
77
|
+
tries -= 1
|
78
|
+
raise "could not validate" if tries.zero?
|
79
|
+
sleep(delay)
|
80
|
+
retry
|
81
|
+
end
|
82
|
+
|
83
|
+
def inspect
|
84
|
+
"#<%s:0x%08x glob=%s>" % [self.class, object_id<<1, @glob.inspect]
|
85
|
+
end
|
86
|
+
end
|