butler 1.8.0 → 1.8.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|