butler 1.8.3 → 1.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.txt +293 -37
- data/README.txt +10 -0
- data/Rakefile +24 -13
- data/bin/botcontrol +6 -5
- data/data/butler/dialogs/create.rb +21 -6
- data/data/butler/dialogs/create_config.rb +5 -2
- data/data/butler/dialogs/en/create.yaml +6 -3
- data/data/butler/dialogs/en/create_config.yaml +1 -0
- data/data/butler/dialogs/en/quickcreate.yaml +1 -1
- data/data/butler/dialogs/en/sync.yaml +7 -0
- data/data/butler/dialogs/en/username.yaml +2 -0
- data/data/butler/dialogs/quickcreate.rb +6 -2
- data/data/butler/dialogs/sync.rb +83 -0
- data/data/butler/dialogs/username.rb +1 -0
- data/data/butler/plugins/core/ping.rb +22 -0
- data/data/butler/plugins/core/remote.rb +38 -0
- data/data/butler/plugins/dev/eval.rb +6 -4
- data/data/butler/plugins/dev/onhandlers.rb +93 -0
- data/data/butler/plugins/dev/rawlog.rb +109 -45
- data/data/butler/plugins/games/countdown.rb +23 -14
- data/data/butler/plugins/games/eightball.rb +21 -13
- data/data/butler/plugins/games/roll.rb +12 -12
- data/data/butler/plugins/irc/join.rb +2 -2
- data/data/butler/plugins/irc/notice.rb +10 -10
- data/data/butler/plugins/irc/part.rb +12 -12
- data/data/butler/plugins/irc/privmsg.rb +10 -10
- data/data/butler/plugins/irc/quit.rb +12 -12
- data/data/butler/plugins/operator/devoice.rb +1 -1
- data/data/butler/plugins/public/help.rb +10 -4
- data/data/butler/plugins/{util → service}/calculator.rb +0 -0
- data/data/butler/plugins/service/define.rb +16 -13
- data/data/butler/plugins/service/log.rb +85 -0
- data/data/butler/plugins/service/seen.rb +64 -0
- data/data/butler/plugins/service/svn.rb +6 -5
- data/data/butler/plugins/util/load.rb +3 -1
- data/data/butler/services/org.rubyforge.butler/calculator/1/service.rb +96 -0
- data/data/butler/services/org.rubyforge.butler/log/1/service.rb +148 -68
- data/lib/access/admin.rb +5 -0
- data/lib/blank.rb +32 -0
- data/lib/butler.rb +4 -4
- data/lib/butler/bot.rb +118 -33
- data/lib/butler/control.rb +5 -4
- data/lib/butler/debuglog.rb +12 -4
- data/lib/butler/dialog.rb +1 -1
- data/lib/butler/initialvalues.rb +1 -1
- data/lib/butler/irc/client.rb +31 -12
- data/lib/butler/irc/message.rb +32 -13
- data/lib/butler/irc/parser.rb +67 -30
- data/lib/butler/irc/parser/{commands.rb → command.rb} +0 -38
- data/lib/butler/irc/parser/generic.rb +9 -12
- data/lib/butler/irc/parser/rfc2812.rb +40 -2
- data/lib/butler/irc/socket.rb +66 -41
- data/lib/butler/irc/string.rb +1 -5
- data/lib/butler/plugin.rb +56 -23
- data/lib/butler/plugin/configproxy.rb +1 -0
- data/lib/butler/plugin/more.rb +2 -2
- data/lib/butler/plugins.rb +7 -1
- data/lib/butler/remote/connection.rb +113 -0
- data/lib/butler/remote/message.rb +157 -0
- data/lib/butler/remote/server.rb +85 -0
- data/lib/butler/remote/user.rb +46 -0
- data/lib/butler/service.rb +2 -1
- data/lib/butler/services.rb +2 -2
- data/lib/butler/version.rb +2 -2
- data/lib/configuration.rb +13 -16
- data/lib/ostructfixed.rb +0 -6
- data/lib/ruby/array/random.rb +2 -1
- data/lib/scriptfile.rb +63 -14
- data/lib/timingoutresource.rb +54 -0
- data/test/test_scriptfile.rb +51 -0
- metadata +63 -61
- data/data/butler/dialogs/en/sync_plugins.yaml +0 -3
- data/data/butler/dialogs/sync_plugins.rb +0 -30
- data/data/butler/services/org.rubyforge.butler/calculator/1/calculator.rb +0 -68
- data/lib/log/splitter.rb +0 -30
- data/test/test_access/privilege/banners.statistics.yaml +0 -3
- data/test/test_access/privilege/banners.yaml +0 -3
- data/test/test_access/privilege/news.create.yaml +0 -3
- data/test/test_access/privilege/news.delete.yaml +0 -3
- data/test/test_access/privilege/news.edit.yaml +0 -3
- data/test/test_access/privilege/news.read.yaml +0 -3
- data/test/test_access/privilege/news.yaml +0 -3
- data/test/test_access/privilege/paid_content.yaml +0 -3
- data/test/test_access/privilege/statistics.ftp.yaml +0 -3
- data/test/test_access/privilege/statistics.web.yaml +0 -3
- data/test/test_access/privilege/statistics.yaml +0 -3
- data/test/test_access/role/chiefeditor.yaml +0 -7
- data/test/test_access/role/editor.yaml +0 -9
- data/test/test_access/user/test.yaml +0 -12
data/lib/butler/irc/string.rb
CHANGED
@@ -46,10 +46,6 @@ class String
|
|
46
46
|
strip_user_prefixes.valid_nickname?
|
47
47
|
end
|
48
48
|
|
49
|
-
def same_nick?(other)
|
50
|
-
strip_user_prefixes.downcase == other.to_str.strip_user_prefixes.downcase
|
51
|
-
end
|
52
|
-
|
53
49
|
# removes indicators from nicknames and channelnames
|
54
50
|
def strip_user_prefixes
|
55
51
|
prefixes = Butler::IRC::User::PREFIXES.dup
|
@@ -126,7 +122,7 @@ class String
|
|
126
122
|
|
127
123
|
Note: not every font/size combination displays bold/italic text.
|
128
124
|
=end
|
129
|
-
def mirc_formatted
|
125
|
+
def mirc_formatted
|
130
126
|
self.gsub( /!\[(.*?)\]/ ) do |match|
|
131
127
|
codes = $1.downcase
|
132
128
|
repl = ""
|
data/lib/butler/plugin.rb
CHANGED
@@ -42,8 +42,9 @@ class Butler
|
|
42
42
|
@name = File.basename(base).freeze
|
43
43
|
@commands = []
|
44
44
|
@listener = []
|
45
|
+
@schedules = []
|
45
46
|
@mapping_type = Hash.new { |h,k| MappingTypes[k] }
|
46
|
-
@config = ConfigProxy.new(@butler.config, "
|
47
|
+
@config = ConfigProxy.new(@butler.config, "plugins/#{base}")
|
47
48
|
|
48
49
|
if File.directory?(path) then
|
49
50
|
raise "Not supported yet"
|
@@ -84,18 +85,20 @@ class Butler
|
|
84
85
|
|
85
86
|
# a class instance variable for the plugin
|
86
87
|
# defines attr_readers for it
|
87
|
-
def plugin_attribute(
|
88
|
+
def plugin_attribute(name, initial_value=nil)
|
88
89
|
(class <<self; self; end).instance_eval {
|
89
|
-
attr_reader(
|
90
|
+
attr_reader(name)
|
90
91
|
}
|
92
|
+
instance_variable_set(:"@#{name}", initial_value)
|
91
93
|
end
|
92
94
|
|
93
95
|
# a class instance variable for the plugin,
|
94
96
|
# defines attr_accessors for it
|
95
|
-
def plugin_accessor(
|
97
|
+
def plugin_accessor(name, initial_value=nil)
|
96
98
|
(class <<self; self; end).instance_eval {
|
97
|
-
attr_accessor(
|
99
|
+
attr_accessor(name)
|
98
100
|
}
|
101
|
+
instance_variable_set(:"@#{name}", initial_value)
|
99
102
|
end
|
100
103
|
|
101
104
|
def create_templates(tmpl, name) # :nodoc:
|
@@ -149,15 +152,21 @@ class Butler
|
|
149
152
|
end
|
150
153
|
|
151
154
|
def every(*args, &block)
|
152
|
-
@butler.scheduler.every(*args, &block)
|
155
|
+
scheduler = @butler.scheduler.every(*args, &block)
|
156
|
+
@schedules << scheduler
|
157
|
+
scheduler
|
153
158
|
end
|
154
159
|
|
155
160
|
def at(*args, &block)
|
156
|
-
@butler.scheduler.at(*args, &block)
|
161
|
+
scheduler = @butler.scheduler.at(*args, &block)
|
162
|
+
@schedules << scheduler
|
163
|
+
scheduler
|
157
164
|
end
|
158
165
|
|
159
166
|
def timed(*args, &block)
|
160
|
-
@butler.scheduler.timed(*args, &block)
|
167
|
+
scheduler = @butler.scheduler.timed(*args, &block)
|
168
|
+
@schedules << scheduler
|
169
|
+
scheduler
|
161
170
|
end
|
162
171
|
|
163
172
|
def subscribe(*args, &block)
|
@@ -179,6 +188,7 @@ class Butler
|
|
179
188
|
info("Unloading plugin '#{@base}' (#{self})")
|
180
189
|
@commands.each { |command| @butler.delete_command(command) }
|
181
190
|
@listener.each { |listener| listener.unsubscribe }
|
191
|
+
@schedules.each { |scheduler| scheduler.finished }
|
182
192
|
end
|
183
193
|
|
184
194
|
end
|
@@ -243,24 +253,47 @@ class Butler
|
|
243
253
|
# can see the rest of the text.
|
244
254
|
def answer(text, vars={})
|
245
255
|
text = localize(text, vars) if text.kind_of?(Symbol)
|
246
|
-
@message.
|
247
|
-
@message
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
256
|
+
if @message.remote? then
|
257
|
+
@message.answer(text)
|
258
|
+
else
|
259
|
+
@message.from.session["more"] = More.new(
|
260
|
+
@message,
|
261
|
+
nil,
|
262
|
+
text.mirc_formatted
|
263
|
+
)
|
264
|
+
@message.answer("#{@message.from.to_s+': ' if @message.public?}#{@message.from.session['more'].show}")
|
265
|
+
end
|
252
266
|
end
|
253
267
|
|
254
268
|
# Same as answer, but 'more' gets a lead prefixed (see Butler::Plugin::More for more info)
|
255
|
-
def answer_with_lead(lead,
|
269
|
+
def answer_with_lead(lead, text, vars={})
|
270
|
+
lead = localize(lead, vars) if lead.kind_of?(Symbol)
|
256
271
|
text = localize(text, vars) if text.kind_of?(Symbol)
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
272
|
+
if @message.remote? then
|
273
|
+
@message.answer(lead+text) # FIXME, what does More use to concat lead & text?
|
274
|
+
else
|
275
|
+
@message.from.session["more"] = More.new(
|
276
|
+
@message,
|
277
|
+
lead.mirc_formatted,
|
278
|
+
text.mirc_formatted
|
279
|
+
)
|
280
|
+
@message.answer("#{@message.from.to_s+': ' if @message.public?}#{@message.from.session['more'].show}")
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
# Same as answer, but will not prefix the nick in public messages
|
285
|
+
def say(text, vars={})
|
286
|
+
text = localize(text, vars) if text.kind_of?(Symbol)
|
287
|
+
if @message.remote? then
|
288
|
+
@message.answer(text)
|
289
|
+
else
|
290
|
+
@message.from.session["more"] = More.new(
|
291
|
+
@message,
|
292
|
+
nil,
|
293
|
+
text.mirc_formatted
|
294
|
+
)
|
295
|
+
@message.answer(@message.from.session['more'].show)
|
296
|
+
end
|
264
297
|
end
|
265
298
|
|
266
299
|
|
@@ -300,4 +333,4 @@ class Butler
|
|
300
333
|
@butler.notice(string.mirc_formatted, *args)
|
301
334
|
end
|
302
335
|
end
|
303
|
-
end
|
336
|
+
end
|
data/lib/butler/plugin/more.rb
CHANGED
@@ -9,7 +9,7 @@
|
|
9
9
|
class Butler
|
10
10
|
class Plugin
|
11
11
|
class More
|
12
|
-
MessageLength =
|
12
|
+
MessageLength = 500
|
13
13
|
String = {
|
14
14
|
"en" => "![c(white),(black)]more...![o]".mirc_formatted,
|
15
15
|
"de" => "![c(white),(black)]mehr...![o]".mirc_formatted,
|
@@ -54,7 +54,7 @@ class Butler
|
|
54
54
|
|
55
55
|
# show adds lead and tail depending on requirement (no tail for last message, no lead if empty)
|
56
56
|
def show
|
57
|
-
"#{lead+': ' if lead}#{@pieces[@index]}#{'
|
57
|
+
"#{lead+': ' if lead}#{@pieces[@index]}#{' '+tail if succ?}"
|
58
58
|
end
|
59
59
|
|
60
60
|
def tail
|
data/lib/butler/plugins.rb
CHANGED
@@ -102,6 +102,8 @@ class Butler
|
|
102
102
|
end
|
103
103
|
begin
|
104
104
|
unload(base) if loaded?(base) # a plugin may not be loaded twice
|
105
|
+
rescue Interrupt
|
106
|
+
raise
|
105
107
|
rescue Exception => e
|
106
108
|
exception(e)
|
107
109
|
end
|
@@ -114,6 +116,8 @@ class Butler
|
|
114
116
|
plugin.load_plugin(@butler, base, path)
|
115
117
|
plugin.class_eval(File.read(codefile), codefile)
|
116
118
|
plugin.on_load
|
119
|
+
rescue Interrupt
|
120
|
+
raise
|
117
121
|
rescue Exception => e
|
118
122
|
e.extend Exception::Detailed
|
119
123
|
e.prepend "Loading plugin #{base} failed."
|
@@ -126,8 +130,10 @@ class Butler
|
|
126
130
|
|
127
131
|
def unload(base)
|
128
132
|
begin
|
129
|
-
@plugins[base].on_unload
|
130
133
|
@plugins[base].unload_plugin
|
134
|
+
@plugins[base].on_unload
|
135
|
+
rescue Interrupt
|
136
|
+
raise
|
131
137
|
rescue Exception => e
|
132
138
|
e.extend Exception::Detailed
|
133
139
|
e.prepend "Exception raised while unloading plugin #{base}."
|
@@ -0,0 +1,113 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2007 by Stefan Rusterholz.
|
3
|
+
# All rights reserved.
|
4
|
+
# See LICENSE.txt for permissions.
|
5
|
+
#++
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
require 'timeout'
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
class Butler
|
14
|
+
module Remote
|
15
|
+
class Connection
|
16
|
+
def self.open_thread(server, socket)
|
17
|
+
connection = new(server, socket)
|
18
|
+
Thread.new {
|
19
|
+
connection.start
|
20
|
+
connection.read_loop
|
21
|
+
}
|
22
|
+
connection
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.open(server, socket)
|
26
|
+
connection = new(server, socket)
|
27
|
+
connection.start
|
28
|
+
connection
|
29
|
+
end
|
30
|
+
|
31
|
+
include Log::Comfort
|
32
|
+
|
33
|
+
attr_reader :user
|
34
|
+
|
35
|
+
def initialize(server, socket)
|
36
|
+
@server = server
|
37
|
+
@socket = socket
|
38
|
+
@user = nil
|
39
|
+
@logger = nil
|
40
|
+
end
|
41
|
+
|
42
|
+
def start
|
43
|
+
puts("Please identify yourself")
|
44
|
+
tries = 3
|
45
|
+
access = nil
|
46
|
+
username = nil
|
47
|
+
timeout(180) {
|
48
|
+
tries.times { |i|
|
49
|
+
@socket.print("Username: ")
|
50
|
+
username = @socket.gets.chomp
|
51
|
+
@socket.print("Password: ")
|
52
|
+
password = @socket.gets.chomp
|
53
|
+
access = @server.login(username, password)
|
54
|
+
break if access
|
55
|
+
sleep(i)
|
56
|
+
puts("Failed to authenticate, please try again.")
|
57
|
+
}
|
58
|
+
}
|
59
|
+
return quit("Failed to authenticate") unless access
|
60
|
+
puts "You're logged in, welcome #{username}."
|
61
|
+
puts "To terminate the session, enter an empty line."
|
62
|
+
puts ""
|
63
|
+
@user = User.new(username, access)
|
64
|
+
rescue Timeout::Error
|
65
|
+
quit("Login timed out.")
|
66
|
+
rescue Exception => e
|
67
|
+
exception(e)
|
68
|
+
quit(nil)
|
69
|
+
end
|
70
|
+
|
71
|
+
def read_loop
|
72
|
+
while line = @socket.gets
|
73
|
+
line.chomp!
|
74
|
+
p line
|
75
|
+
break if line.empty?
|
76
|
+
begin
|
77
|
+
@server.dispatch(Message.new(@server, self, @user, line))
|
78
|
+
|
79
|
+
# on these errors we got to get out of the loop
|
80
|
+
rescue Errno::EPIPE, SocketError
|
81
|
+
raise
|
82
|
+
# all others should not put the read-thread in any danger
|
83
|
+
rescue Exception => e
|
84
|
+
exception(e)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
rescue Exception => e
|
88
|
+
exception(e)
|
89
|
+
ensure
|
90
|
+
@server.disconnected(self)
|
91
|
+
close
|
92
|
+
end
|
93
|
+
|
94
|
+
def puts(*text)
|
95
|
+
@socket.puts(*text)
|
96
|
+
end
|
97
|
+
|
98
|
+
def quit(reason=nil)
|
99
|
+
puts reason if reason
|
100
|
+
close
|
101
|
+
@server.disconnected(self)
|
102
|
+
end
|
103
|
+
|
104
|
+
def closed?
|
105
|
+
@socket.closed?
|
106
|
+
end
|
107
|
+
|
108
|
+
def close
|
109
|
+
@socket.close unless @socket.closed?
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,157 @@
|
|
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
|
+
module IRC
|
11
|
+
class Message
|
12
|
+
def remote?
|
13
|
+
false
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module Remote
|
19
|
+
# Butler::Remote::Message emulates a Butler::IRC::Message
|
20
|
+
class Message
|
21
|
+
Command = "privmsg".freeze
|
22
|
+
|
23
|
+
attr_reader :text
|
24
|
+
attr_reader :from
|
25
|
+
|
26
|
+
# the language of this message, as determined by butler
|
27
|
+
attr_accessor :language
|
28
|
+
|
29
|
+
# the invocation-sequence (e.g. "butler, "), nil if none was used (e.g. in
|
30
|
+
# private messages)
|
31
|
+
attr_accessor :invocation
|
32
|
+
|
33
|
+
def initialize(server, client, from, text)
|
34
|
+
@server = server
|
35
|
+
@client = client
|
36
|
+
@from = from
|
37
|
+
@text = text
|
38
|
+
@language = nil
|
39
|
+
@invocation = nil
|
40
|
+
@arguments = nil
|
41
|
+
@post_arguments = nil
|
42
|
+
end
|
43
|
+
|
44
|
+
# FIXME complete
|
45
|
+
def initialize_copy(original) #:nodoc:
|
46
|
+
super
|
47
|
+
@server = original.instance_variable_get(:@server)
|
48
|
+
@client = original.instance_variable_get(:@client)
|
49
|
+
@from = original.instance_variable_get(:@from)
|
50
|
+
@text = original.text.dup
|
51
|
+
end
|
52
|
+
|
53
|
+
# parses a given string into argument-tokens. a token is either a word or a quoted string. escapes are respected.
|
54
|
+
# e.g. 'Hallo "this is token2" "and this \"token\" is token3"'
|
55
|
+
# would be parsed into: ["hallo", "this is token2", "and this \"token\" is token3"]
|
56
|
+
def arguments
|
57
|
+
return [] unless text # only messages with text can be tokenized to parameters
|
58
|
+
@arguments ||= begin # don't do double-work
|
59
|
+
text[@invocation.length..-1].arguments
|
60
|
+
rescue String::SingleQuoteException => ex
|
61
|
+
ex.pre+[ex.post.join(" ")]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# FIXME due to it's current working, post_arguments will strip formatting
|
66
|
+
# Returns the rest of message text after argument
|
67
|
+
# == Synopsis
|
68
|
+
# "foo bar baz".post_arguments[0] # => "foo bar baz"
|
69
|
+
# "foo bar baz".post_arguments[1] # => "bar baz"
|
70
|
+
# "foo bar baz".post_arguments[2] # => "baz"
|
71
|
+
def post_arguments
|
72
|
+
return [] unless text # only messages with text can be tokenized to parameters
|
73
|
+
@post_arguments ||= text[@invocation.length..-1].post_arguments # don't do double-work
|
74
|
+
end
|
75
|
+
|
76
|
+
alias raw text
|
77
|
+
alias params text
|
78
|
+
|
79
|
+
def symbol
|
80
|
+
:PRIVMSG
|
81
|
+
end
|
82
|
+
|
83
|
+
def prefix
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
|
87
|
+
def command
|
88
|
+
Command
|
89
|
+
end
|
90
|
+
|
91
|
+
def realm
|
92
|
+
:private
|
93
|
+
end
|
94
|
+
|
95
|
+
def remote?
|
96
|
+
true
|
97
|
+
end
|
98
|
+
|
99
|
+
def private?
|
100
|
+
true
|
101
|
+
end
|
102
|
+
|
103
|
+
def public?
|
104
|
+
false
|
105
|
+
end
|
106
|
+
|
107
|
+
def answer(text)
|
108
|
+
@client.puts text.mirc_stripped
|
109
|
+
end
|
110
|
+
|
111
|
+
def identified?
|
112
|
+
true
|
113
|
+
end
|
114
|
+
|
115
|
+
def for
|
116
|
+
@server.myself
|
117
|
+
end
|
118
|
+
|
119
|
+
def channel
|
120
|
+
nil
|
121
|
+
end
|
122
|
+
|
123
|
+
def text
|
124
|
+
@text
|
125
|
+
end
|
126
|
+
|
127
|
+
# access to raw parsed data
|
128
|
+
def [](index)
|
129
|
+
nil
|
130
|
+
end
|
131
|
+
|
132
|
+
def []=(index, value)
|
133
|
+
raise NoMethodError, "Can't write to #{index}" # unless respond_to?("#{index}=")
|
134
|
+
end
|
135
|
+
|
136
|
+
def has_key?(key)
|
137
|
+
false #@fields.has_key?(key.to_sym)
|
138
|
+
end
|
139
|
+
|
140
|
+
# change charset encoding
|
141
|
+
def transcode!(from, to)
|
142
|
+
end
|
143
|
+
|
144
|
+
def ===(other)
|
145
|
+
case other
|
146
|
+
when Symbol: :PRIVMSG == other
|
147
|
+
when Regexp: @text =~ other
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# return a hash of the fields
|
152
|
+
def to_hash
|
153
|
+
raise NoMethodError
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|