butler 1.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +4 -0
- data/GPL.txt +340 -0
- data/LICENSE.txt +52 -0
- data/README +37 -0
- data/Rakefile +334 -0
- data/bin/botcontrol +230 -0
- data/data/butler/config_template.yaml +4 -0
- data/data/butler/dialogs/backup.rb +19 -0
- data/data/butler/dialogs/botcontrol.rb +4 -0
- data/data/butler/dialogs/config.rb +1 -0
- data/data/butler/dialogs/create.rb +53 -0
- data/data/butler/dialogs/delete.rb +3 -0
- data/data/butler/dialogs/en/backup.yaml +6 -0
- data/data/butler/dialogs/en/botcontrol.yaml +5 -0
- data/data/butler/dialogs/en/create.yaml +11 -0
- data/data/butler/dialogs/en/delete.yaml +2 -0
- data/data/butler/dialogs/en/help.yaml +17 -0
- data/data/butler/dialogs/en/info.yaml +13 -0
- data/data/butler/dialogs/en/list.yaml +4 -0
- data/data/butler/dialogs/en/notyetimplemented.yaml +2 -0
- data/data/butler/dialogs/en/rename.yaml +3 -0
- data/data/butler/dialogs/en/start.yaml +3 -0
- data/data/butler/dialogs/en/sync_plugins.yaml +3 -0
- data/data/butler/dialogs/en/uninstall.yaml +5 -0
- data/data/butler/dialogs/en/unknown_command.yaml +2 -0
- data/data/butler/dialogs/help.rb +11 -0
- data/data/butler/dialogs/info.rb +27 -0
- data/data/butler/dialogs/interactive.rb +1 -0
- data/data/butler/dialogs/list.rb +10 -0
- data/data/butler/dialogs/notyetimplemented.rb +1 -0
- data/data/butler/dialogs/rename.rb +4 -0
- data/data/butler/dialogs/selectbot.rb +2 -0
- data/data/butler/dialogs/start.rb +5 -0
- data/data/butler/dialogs/sync_plugins.rb +30 -0
- data/data/butler/dialogs/uninstall.rb +17 -0
- data/data/butler/dialogs/unknown_command.rb +1 -0
- data/data/butler/plugins/core/logout.rb +41 -0
- data/data/butler/plugins/core/plugins.rb +134 -0
- data/data/butler/plugins/core/privilege.rb +103 -0
- data/data/butler/plugins/core/user.rb +166 -0
- data/data/butler/plugins/dev/eval.rb +64 -0
- data/data/butler/plugins/dev/nometa.rb +14 -0
- data/data/butler/plugins/dev/onhandlers.rb +93 -0
- data/data/butler/plugins/dev/raw.rb +36 -0
- data/data/butler/plugins/dev/rawlog.rb +77 -0
- data/data/butler/plugins/games/eightball.rb +54 -0
- data/data/butler/plugins/games/mastermind.rb +174 -0
- data/data/butler/plugins/irc/action.rb +36 -0
- data/data/butler/plugins/irc/join.rb +38 -0
- data/data/butler/plugins/irc/notice.rb +36 -0
- data/data/butler/plugins/irc/part.rb +38 -0
- data/data/butler/plugins/irc/privmsg.rb +36 -0
- data/data/butler/plugins/irc/quit.rb +36 -0
- data/data/butler/plugins/operator/deop.rb +41 -0
- data/data/butler/plugins/operator/devoice.rb +41 -0
- data/data/butler/plugins/operator/limit.rb +47 -0
- data/data/butler/plugins/operator/op.rb +41 -0
- data/data/butler/plugins/operator/voice.rb +41 -0
- data/data/butler/plugins/public/help.rb +69 -0
- data/data/butler/plugins/public/login.rb +72 -0
- data/data/butler/plugins/public/usage.rb +49 -0
- data/data/butler/plugins/service/clones.rb +56 -0
- data/data/butler/plugins/service/define.rb +47 -0
- data/data/butler/plugins/service/log.rb +183 -0
- data/data/butler/plugins/service/svn.rb +91 -0
- data/data/butler/plugins/util/cycle.rb +98 -0
- data/data/butler/plugins/util/load.rb +41 -0
- data/data/butler/plugins/util/pong.rb +29 -0
- data/data/butler/strings/random/acknowledge.en.yaml +5 -0
- data/data/butler/strings/random/gratitude.en.yaml +3 -0
- data/data/butler/strings/random/hello.en.yaml +4 -0
- data/data/butler/strings/random/ignorance.en.yaml +7 -0
- data/data/butler/strings/random/ignorance_about.en.yaml +3 -0
- data/data/butler/strings/random/insult.en.yaml +3 -0
- data/data/butler/strings/random/rejection.en.yaml +12 -0
- data/data/man/botcontrol.1 +17 -0
- data/lib/access.rb +187 -0
- data/lib/access/admin.rb +16 -0
- data/lib/access/privilege.rb +122 -0
- data/lib/access/role.rb +102 -0
- data/lib/access/savable.rb +18 -0
- data/lib/access/user.rb +180 -0
- data/lib/access/yamlbase.rb +126 -0
- data/lib/butler.rb +188 -0
- data/lib/butler/bot.rb +247 -0
- data/lib/butler/control.rb +93 -0
- data/lib/butler/dialog.rb +64 -0
- data/lib/butler/initialvalues.rb +40 -0
- data/lib/butler/irc/channel.rb +135 -0
- data/lib/butler/irc/channels.rb +96 -0
- data/lib/butler/irc/client.rb +351 -0
- data/lib/butler/irc/hostmask.rb +53 -0
- data/lib/butler/irc/message.rb +184 -0
- data/lib/butler/irc/parser.rb +125 -0
- data/lib/butler/irc/parser/commands.rb +83 -0
- data/lib/butler/irc/parser/generic.rb +343 -0
- data/lib/butler/irc/socket.rb +378 -0
- data/lib/butler/irc/string.rb +186 -0
- data/lib/butler/irc/topic.rb +15 -0
- data/lib/butler/irc/user.rb +265 -0
- data/lib/butler/irc/users.rb +112 -0
- data/lib/butler/plugin.rb +249 -0
- data/lib/butler/plugin/configproxy.rb +35 -0
- data/lib/butler/plugin/mapper.rb +85 -0
- data/lib/butler/plugin/matcher.rb +55 -0
- data/lib/butler/plugin/onhandlers.rb +70 -0
- data/lib/butler/plugin/trigger.rb +58 -0
- data/lib/butler/plugins.rb +147 -0
- data/lib/butler/version.rb +17 -0
- data/lib/cloptions.rb +217 -0
- data/lib/cloptions/adapters.rb +24 -0
- data/lib/cloptions/switch.rb +132 -0
- data/lib/configuration.rb +223 -0
- data/lib/dialogline.rb +296 -0
- data/lib/dialogline/localizations.rb +24 -0
- data/lib/durations.rb +57 -0
- data/lib/event.rb +295 -0
- data/lib/event/at.rb +64 -0
- data/lib/event/every.rb +56 -0
- data/lib/event/timed.rb +112 -0
- data/lib/installer.rb +75 -0
- data/lib/iterator.rb +34 -0
- data/lib/log.rb +68 -0
- data/lib/log/comfort.rb +85 -0
- data/lib/log/converter.rb +23 -0
- data/lib/log/entry.rb +152 -0
- data/lib/log/fakeio.rb +55 -0
- data/lib/log/file.rb +54 -0
- data/lib/log/filereader.rb +81 -0
- data/lib/log/forward.rb +49 -0
- data/lib/log/methods.rb +39 -0
- data/lib/log/nolog.rb +18 -0
- data/lib/log/splitter.rb +26 -0
- data/lib/ostructfixed.rb +26 -0
- data/lib/ruby/array/columnize.rb +38 -0
- data/lib/ruby/dir/mktree.rb +28 -0
- data/lib/ruby/enumerable/join.rb +13 -0
- data/lib/ruby/exception/detailed.rb +24 -0
- data/lib/ruby/file/append.rb +11 -0
- data/lib/ruby/file/write.rb +11 -0
- data/lib/ruby/hash/zip.rb +15 -0
- data/lib/ruby/kernel/bench.rb +15 -0
- data/lib/ruby/kernel/daemonize.rb +42 -0
- data/lib/ruby/kernel/non_verbose.rb +17 -0
- data/lib/ruby/kernel/safe_fork.rb +18 -0
- data/lib/ruby/range/stepped.rb +11 -0
- data/lib/ruby/string/arguments.rb +72 -0
- data/lib/ruby/string/chunks.rb +15 -0
- data/lib/ruby/string/post_arguments.rb +44 -0
- data/lib/ruby/string/unescaped.rb +17 -0
- data/lib/scheduler.rb +164 -0
- data/lib/scriptfile.rb +101 -0
- data/lib/templater.rb +86 -0
- data/test/cloptions.rb +134 -0
- data/test/cv.rb +28 -0
- data/test/irc/client.rb +85 -0
- data/test/irc/client_login.txt +53 -0
- data/test/irc/client_subscribe.txt +8 -0
- data/test/irc/message.rb +30 -0
- data/test/irc/messages.txt +64 -0
- data/test/irc/parser.rb +13 -0
- data/test/irc/profile_parser.rb +12 -0
- data/test/irc/users.rb +28 -0
- metadata +256 -0
data/lib/dialogline.rb
ADDED
@@ -0,0 +1,296 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2007 by Stefan Rusterholz.
|
3
|
+
# All rights reserved.
|
4
|
+
# See LICENSE.txt for permissions.
|
5
|
+
#++
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
require 'erb'
|
10
|
+
require 'dialogline/localizations'
|
11
|
+
begin
|
12
|
+
require 'readline'
|
13
|
+
rescue LoadError; warn "DialogLine starts without readline support." end
|
14
|
+
|
15
|
+
class DialogLine
|
16
|
+
module VERSION
|
17
|
+
MAJOR = 0
|
18
|
+
MINOR = 0
|
19
|
+
TINY = 1
|
20
|
+
STRING = "#{MAJOR}.#{MINOR}.#{TINY}"
|
21
|
+
end
|
22
|
+
|
23
|
+
class EndOfInput < RuntimeError; end
|
24
|
+
|
25
|
+
class Variables
|
26
|
+
def initialize(data, fallback=nil)
|
27
|
+
@fallback = fallback
|
28
|
+
@data = (@fallback ? Hash.new { |h,k| @fallback.send(k) } : {}).merge(data)
|
29
|
+
end
|
30
|
+
|
31
|
+
def __keys__(include_fallback=true)
|
32
|
+
@data.keys + ((include_fallback && @fallback)? @fallback.__keys__ : [])
|
33
|
+
end
|
34
|
+
|
35
|
+
def has_key?(key)
|
36
|
+
@data.has_key?(key) || (@fallback && @fallback.has_key?(key))
|
37
|
+
end
|
38
|
+
|
39
|
+
def method_missing(m, *args)
|
40
|
+
case args.length
|
41
|
+
when 0: return @data[m] if has_key?(m)
|
42
|
+
when 1: return @data[m.to_s[0..-1].to_sym] = args.first if m.to_s =~ /=\z/
|
43
|
+
end
|
44
|
+
super
|
45
|
+
end
|
46
|
+
|
47
|
+
def binding
|
48
|
+
super
|
49
|
+
end
|
50
|
+
|
51
|
+
def inspect
|
52
|
+
"#<%s:0x%08x @data=%s @fallback=%s>" % [
|
53
|
+
self.class,
|
54
|
+
object_id,
|
55
|
+
@data.inspect,
|
56
|
+
@fallback ? "#<%s:0x%08x ...>" % [@fallback.class, @fallback.object_id] : "nil"
|
57
|
+
]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
attr_reader :language
|
62
|
+
attr_reader :dir
|
63
|
+
attr_reader :variables
|
64
|
+
|
65
|
+
def initialize(dir, lang=nil, variables={})
|
66
|
+
@dir = dir
|
67
|
+
@language = lang
|
68
|
+
@in = $stdin
|
69
|
+
@out = $stdout
|
70
|
+
@variables = Variables.new(variables)
|
71
|
+
end
|
72
|
+
|
73
|
+
def puts(*args)
|
74
|
+
@out.puts(*args)
|
75
|
+
end
|
76
|
+
|
77
|
+
def print(*args)
|
78
|
+
@out.print(*args)
|
79
|
+
@out.flush
|
80
|
+
end
|
81
|
+
|
82
|
+
def printf(*args)
|
83
|
+
@out.printf(*args)
|
84
|
+
@out.flush
|
85
|
+
end
|
86
|
+
|
87
|
+
def localized(index)
|
88
|
+
Localized[index][@language]
|
89
|
+
end
|
90
|
+
|
91
|
+
def request(string, default=nil, term=": ")
|
92
|
+
if default then
|
93
|
+
print("#{string} [#{default}]#{term}")
|
94
|
+
else
|
95
|
+
print("#{string}#{term}")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def gets(chomp=true)
|
100
|
+
raise EndOfInput unless response = @in.gets
|
101
|
+
response.chomp! if chomp
|
102
|
+
response
|
103
|
+
end
|
104
|
+
|
105
|
+
def in_context(name)
|
106
|
+
Context.new([@in,@out],@language,@dir,name,false)
|
107
|
+
end
|
108
|
+
|
109
|
+
def singleton_def(name, &code)
|
110
|
+
singleton = class<<self;self;end
|
111
|
+
singleton.send(:define_method, name, &code)
|
112
|
+
end
|
113
|
+
|
114
|
+
def discuss(file, use_ostruct=false, variables={}, &stuff)
|
115
|
+
instance_eval(&stuff) if stuff
|
116
|
+
Context.run(self, file, use_ostruct, variables)
|
117
|
+
end
|
118
|
+
|
119
|
+
class Context
|
120
|
+
(DialogLine.public_instance_methods(false) & Context.private_instance_methods(true)).each { |m|
|
121
|
+
undef_method(m)
|
122
|
+
}
|
123
|
+
|
124
|
+
attr_reader :result
|
125
|
+
attr_reader :variables
|
126
|
+
|
127
|
+
def self.run(*args)
|
128
|
+
context = new(*args)
|
129
|
+
context.result
|
130
|
+
end
|
131
|
+
|
132
|
+
def initialize(dialog, file, use_ostruct=false, variables={})
|
133
|
+
@dialog = dialog
|
134
|
+
@file = file
|
135
|
+
@use_ostruct = use_ostruct
|
136
|
+
dir, lang = dialog.dir, dialog.language
|
137
|
+
@variables = Variables.new(variables, dialog.variables)
|
138
|
+
|
139
|
+
qfile = "#{dir}/#{lang}/#{file}.yaml"
|
140
|
+
dfile = "#{dir}/#{file}.rb"
|
141
|
+
@result = use_ostruct ? OpenStruct.new : {}
|
142
|
+
@questions = Hash.new { |h,k| raise "No Question for #{k} in #{file}" }.merge(
|
143
|
+
File.exist?(qfile) ? YAML.load_file(qfile) : {}
|
144
|
+
)
|
145
|
+
|
146
|
+
run_file(dfile)
|
147
|
+
end
|
148
|
+
|
149
|
+
def run_file(__file__)
|
150
|
+
instance_eval(File.read(__file__), __file__)
|
151
|
+
end
|
152
|
+
private :run_file
|
153
|
+
|
154
|
+
def response(method, name, *args)
|
155
|
+
@result[name] = send(method, name, *args)
|
156
|
+
end
|
157
|
+
|
158
|
+
def store(name, value)
|
159
|
+
@result[name] = value
|
160
|
+
end
|
161
|
+
|
162
|
+
def string(name, variables={})
|
163
|
+
if variables.empty? then
|
164
|
+
variables = @variables
|
165
|
+
else
|
166
|
+
variables = Variables.new(variables, @variables)
|
167
|
+
end
|
168
|
+
ERB.new(@questions[name]).result(variables.binding)
|
169
|
+
end
|
170
|
+
|
171
|
+
def prompt(question, default=nil, vars={}, *args)
|
172
|
+
valid = false
|
173
|
+
yes = localized(:yes)
|
174
|
+
no = localized(:no)
|
175
|
+
until valid
|
176
|
+
default_answer = default ? "#{yes.upcase}/#{no}" : "#{no.upcase}/#{yes}"
|
177
|
+
request("#{string(question, vars)} [#{default_answer}]")
|
178
|
+
response, valid = validate(gets.downcase, default, String, :one_of => [yes, no])
|
179
|
+
puts localized(:invalid) unless valid
|
180
|
+
end
|
181
|
+
response == yes or response == true
|
182
|
+
end
|
183
|
+
|
184
|
+
def say(message, vars={})
|
185
|
+
puts(string(message, vars))
|
186
|
+
end
|
187
|
+
|
188
|
+
def ask(question, default=nil, klass=String, *args)
|
189
|
+
valid = false
|
190
|
+
until valid
|
191
|
+
request(string(question), default)
|
192
|
+
response, valid = validate(gets, default, klass, *args)
|
193
|
+
puts localized(:invalid) unless valid
|
194
|
+
end
|
195
|
+
response
|
196
|
+
end
|
197
|
+
|
198
|
+
# like option, but allows custom entry
|
199
|
+
def suggestion(question, suggestions, default=nil, klass=String, *args)
|
200
|
+
valid = false
|
201
|
+
until valid
|
202
|
+
request(string(question), default, ":\n")
|
203
|
+
suggestions.each_with_index { |opt,i| printf "%2d) %s\n", i+1, opt }
|
204
|
+
response, valid = validate(gets, default, klass, *args)
|
205
|
+
pos = Integer(response) rescue -1
|
206
|
+
response, valid = suggestions[pos-1], true if pos.between?(1, suggestions.length)
|
207
|
+
puts localized(:invalid) unless valid
|
208
|
+
end
|
209
|
+
response
|
210
|
+
end
|
211
|
+
|
212
|
+
def option(question, options, default=nil, klass=String, *args)
|
213
|
+
valid = false
|
214
|
+
until valid
|
215
|
+
request(string(question), default, ":\n")
|
216
|
+
options.each_with_index { |opt,i| printf "%2d) %s\n", i+1, opt }
|
217
|
+
if args.last.kind_of?(Hash) then
|
218
|
+
args.last.merge!(:one_of => options)
|
219
|
+
else
|
220
|
+
args.push(:one_of => options)
|
221
|
+
end
|
222
|
+
response, valid = validate(gets, default, klass, *args)
|
223
|
+
if !valid && (pos = Integer(response) rescue false) then
|
224
|
+
response, valid = options[pos-1], true if pos.between?(1, options.length)
|
225
|
+
end
|
226
|
+
puts localized(:invalid) unless valid
|
227
|
+
end
|
228
|
+
response
|
229
|
+
end
|
230
|
+
|
231
|
+
def context(file, variables={})
|
232
|
+
Context.run(@dialog, file, @use_ostruct, variables)
|
233
|
+
end
|
234
|
+
|
235
|
+
def validate(response, default, klass, *args) # FIXME, use Validator class here
|
236
|
+
return [default, true] if !default.nil? and response.empty?
|
237
|
+
|
238
|
+
case [klass]
|
239
|
+
when [String]: validate_string(response, *args)
|
240
|
+
when [Integer]: validate_integer(response, *args)
|
241
|
+
when [Array]: validate_array(response, *args)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def validate_array(response, klass, *args)
|
246
|
+
response = response.split(/,\s*/)
|
247
|
+
valid = response.all? { |item| validate(item, nil, klass, *args) }
|
248
|
+
return [response, valid]
|
249
|
+
end
|
250
|
+
|
251
|
+
def validate_string(response, opts={})
|
252
|
+
valid = opts.all? { |test, param|
|
253
|
+
case test
|
254
|
+
when :matching: response =~ param
|
255
|
+
when :not_matching: response !~ param
|
256
|
+
when :max: response.length <= param
|
257
|
+
when :min: response.length >= param
|
258
|
+
when :max_exclusive: response.length < param
|
259
|
+
when :min_exclusive: response.length > param
|
260
|
+
when :between: response.length.between?(*param)
|
261
|
+
when :one_of: param.include?(response)
|
262
|
+
when :case_one_of: param.include?(response.downcase)
|
263
|
+
end
|
264
|
+
}
|
265
|
+
return [response, valid]
|
266
|
+
end
|
267
|
+
|
268
|
+
def validate_integer(response, opts={})
|
269
|
+
return [response, false] unless response = Integer(response) rescue nil
|
270
|
+
valid = opts.all? { |test, param|
|
271
|
+
case test
|
272
|
+
when :greater_than: response > param
|
273
|
+
when :less_than: response < param
|
274
|
+
when :greater_or_equal: response >= param
|
275
|
+
when :less_or_equal: response <= param
|
276
|
+
when :between: response.between?(*param)
|
277
|
+
when :one_of: param.include?(response)
|
278
|
+
end
|
279
|
+
}
|
280
|
+
return [response, valid]
|
281
|
+
end
|
282
|
+
|
283
|
+
def method_missing(m, *args, &block)
|
284
|
+
super unless @dialog.respond_to?(m)
|
285
|
+
@dialog.send(m, *args, &block)
|
286
|
+
end
|
287
|
+
|
288
|
+
def to_s
|
289
|
+
"#<%s:0x%X %s>" % [
|
290
|
+
self.class,
|
291
|
+
object_id << 1,
|
292
|
+
@file
|
293
|
+
]
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2007 by Stefan Rusterholz.
|
3
|
+
# All rights reserved.
|
4
|
+
# See LICENSE.txt for permissions.
|
5
|
+
#++
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
class DialogLine
|
10
|
+
Localized = {
|
11
|
+
:invalid => {
|
12
|
+
"de" => "Ungültige Antwort.",
|
13
|
+
"en" => "Invalid response.",
|
14
|
+
},
|
15
|
+
:yes => {
|
16
|
+
"de" => "ja",
|
17
|
+
"en" => "yes",
|
18
|
+
},
|
19
|
+
:no => {
|
20
|
+
"de" => "nein",
|
21
|
+
"en" => "no",
|
22
|
+
},
|
23
|
+
}
|
24
|
+
end
|
data/lib/durations.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2007 by Stefan Rusterholz.
|
3
|
+
# All rights reserved.
|
4
|
+
# See LICENSE.txt for permissions.
|
5
|
+
#++
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
class Numeric
|
10
|
+
# == Synopsis
|
11
|
+
# 45.seconds # => 45
|
12
|
+
def seconds(*args)
|
13
|
+
args.inject(self) { |s,a| s+a }
|
14
|
+
end
|
15
|
+
|
16
|
+
# == Synopsis
|
17
|
+
# 15.minutes # => 900
|
18
|
+
# 15.minutes 45.seconds # => 945
|
19
|
+
def minutes(*args)
|
20
|
+
args.inject(self*60) { |s,a| s+a }
|
21
|
+
end
|
22
|
+
|
23
|
+
# == Synopsis
|
24
|
+
# 5.hours # => 18000
|
25
|
+
# 5.hours 15.minutes, 45.seconds # => 18945
|
26
|
+
def hours(*args)
|
27
|
+
args.inject(self*3600) { |s,a| s+a }
|
28
|
+
end
|
29
|
+
|
30
|
+
# == Synopsis
|
31
|
+
# 2.days # => 172800
|
32
|
+
# 2.days 5.hours, 15.minutes, 45.seconds # => 191745
|
33
|
+
def days(*args)
|
34
|
+
args.inject(self*86400) { |s,a| s+a }
|
35
|
+
end
|
36
|
+
|
37
|
+
# == Synopsis
|
38
|
+
# 1.week # => 604800
|
39
|
+
# 1.week 2.days, 5.hours, 15.minutes, 45.seconds # => 796545
|
40
|
+
def weeks(*args)
|
41
|
+
args.inject(self*604800) { |s,a| s+a }
|
42
|
+
end
|
43
|
+
|
44
|
+
alias second seconds
|
45
|
+
alias minute minutes
|
46
|
+
alias hour hours
|
47
|
+
alias day days
|
48
|
+
alias week weeks
|
49
|
+
|
50
|
+
def ago
|
51
|
+
Time.now-self
|
52
|
+
end
|
53
|
+
|
54
|
+
def from_now
|
55
|
+
Time.now+self
|
56
|
+
end
|
57
|
+
end
|
data/lib/event.rb
ADDED
@@ -0,0 +1,295 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2007 by Stefan Rusterholz.
|
3
|
+
# All rights reserved.
|
4
|
+
# See LICENSE.txt for permissions.
|
5
|
+
#++
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
require 'ruby/range/stepped'
|
10
|
+
require 'event/at'
|
11
|
+
require 'event/every'
|
12
|
+
require 'event/timed'
|
13
|
+
|
14
|
+
|
15
|
+
|
16
|
+
# == Description
|
17
|
+
# Allows to schedule things (furtherly called 'events') in one
|
18
|
+
# of several ways. Scheduled provides the methods to determine
|
19
|
+
# when this event will be executed next, when it was executed
|
20
|
+
# previuosly or how many seconds are left until the next execution.
|
21
|
+
# If a block is provided at creation it will execute that block
|
22
|
+
# everytime the event is due.
|
23
|
+
#
|
24
|
+
# == Synopsis
|
25
|
+
# # calls go_check_mails every 10 minutes, starting in 1h, ending 24h later
|
26
|
+
# check_mail_event = Event.every(600, :start => Time.now+3600, :stop => Time.now+90000) { |event|
|
27
|
+
# go_check_mails(...)
|
28
|
+
# }
|
29
|
+
# # deactivate it for the next 30 minutes
|
30
|
+
# # be aware only calling on/off will leave the Scheduled#next the same
|
31
|
+
# check_mail_event.off
|
32
|
+
# sleep(30*60)
|
33
|
+
#
|
34
|
+
# # avoid that after 'on' the block get's called as many times as it was missed due to off
|
35
|
+
# check_mail_event.postpone
|
36
|
+
# # reactivate the check_mail_event
|
37
|
+
# check_mail_event.on
|
38
|
+
#
|
39
|
+
# # let the mail checking be done 15s earlier this time
|
40
|
+
# check_mail_event.alter(-15)
|
41
|
+
# # see how many seconds are left to the next event
|
42
|
+
# puts check_mail_event.seconds_left.to_s
|
43
|
+
#
|
44
|
+
# # let's the current thread sleep until check_mail_event is finished.
|
45
|
+
# check_mail_event.join
|
46
|
+
#
|
47
|
+
# # this will be due? every hour, at minute 1, 2, 3, 4, 5, at seconds 0 and 30.
|
48
|
+
# # see Scheduled.timed for more information
|
49
|
+
# cron_like = Scheduled.timed("*", (1..5), [0, 30]) { |event| ...do something... }
|
50
|
+
#
|
51
|
+
# # this one will be due? every given
|
52
|
+
# specific_times = Scheduled.at(Time.now+86400, Time.now+3600, Time.now+60)
|
53
|
+
#
|
54
|
+
# # take a break at 09.15, 10.15, 11.15, 13.15, 14.15 and 15.15
|
55
|
+
# takeBreak = Scheduled.timed([9,10,11,13,14,15], [15])
|
56
|
+
#
|
57
|
+
class Event
|
58
|
+
HOUR_DIVISORS = [1, 2, 3, 4, 6, 8, 12, 24]
|
59
|
+
MINUTE_DIVISORS = [1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60]
|
60
|
+
SECOND_DIVISORS = MINUTE_DIVISORS
|
61
|
+
|
62
|
+
include Comparable
|
63
|
+
|
64
|
+
# Time after which the event will be scheduled
|
65
|
+
attr_reader :start
|
66
|
+
|
67
|
+
# Time after which the event won't be scheduled anymore
|
68
|
+
attr_reader :stop
|
69
|
+
|
70
|
+
# Integer or nil, containing how many times the event will be due.
|
71
|
+
attr_reader :times
|
72
|
+
|
73
|
+
# Time object containing the time when the event is due next
|
74
|
+
# Will be nil if there's no next event possible to schedule (technically, not
|
75
|
+
# due to the event being finished, over stop or over times)
|
76
|
+
attr_reader :next
|
77
|
+
|
78
|
+
# Time object containing the time when the event was due the las time
|
79
|
+
attr_reader :previous
|
80
|
+
|
81
|
+
# Integer, counting how many times one_done was called.
|
82
|
+
attr_reader :count
|
83
|
+
|
84
|
+
# :nodoc: FIXME, make it foolproof (no change of responsible scheduler)
|
85
|
+
attr_accessor :scheduler
|
86
|
+
|
87
|
+
class <<self
|
88
|
+
#private :new # FIXME I don't know what the fu* is going on but setting it public in descendants doesn't work here and I don't see why... :-S
|
89
|
+
|
90
|
+
# Creates a Schedule which is invoked in an interval, every <seconds> seconds.
|
91
|
+
# Postponing an every Schedule will set Schedule#next to Time.now+<seconds>.
|
92
|
+
def every(seconds, options={}, &block)
|
93
|
+
Every.new(seconds, options, &block)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Creates a Schedule which is invoked at specified hours, minutes
|
97
|
+
# and seconds of a day. Formats are:
|
98
|
+
# "*" Every (hour, minute, second)
|
99
|
+
# 4 Same as "0/4"
|
100
|
+
# "2/4" Every 4 (hour, minutes, seconds), starting with 2
|
101
|
+
# [1,5] Every specified (hour, minute, second) in the array
|
102
|
+
# 0..5 Same as 0..5.to_a
|
103
|
+
# Postponing a timed Schedule will set @next to the next hour, minute, second-
|
104
|
+
# combination that gives a positive seconds_left.
|
105
|
+
def timed(hours=[0], minutes=[0], seconds=[0], options={}, &block)
|
106
|
+
Timed.new(hours, minutes, seconds, options, &block)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Creates a Schedule that is invoked at specified datetimes.
|
110
|
+
# Postponing an at Schedule will set @next to the first datetime that gives
|
111
|
+
# a positive seconds_left.
|
112
|
+
def at(*datetimes, &block)
|
113
|
+
At.new(datetimes, &block)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def initialize(options={}, &block)
|
118
|
+
@block = block
|
119
|
+
@start = options.delete(:start) || Time.now
|
120
|
+
@stop = options.delete(:stop)
|
121
|
+
@times = options.delete(:times)
|
122
|
+
@finished = false
|
123
|
+
@on = true
|
124
|
+
@wakeup = [] # threads to wakeup when finished
|
125
|
+
|
126
|
+
@previous = nil
|
127
|
+
@next = nil
|
128
|
+
@count = 0
|
129
|
+
end
|
130
|
+
|
131
|
+
def call(*args)
|
132
|
+
@block.call(*args)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Increment @count by one and return @next or nil if event is finished
|
136
|
+
def one_done(increment=true)
|
137
|
+
raise "no next" unless @next
|
138
|
+
raise "Cannot execute deactivated events." unless @on
|
139
|
+
@count += 1 if increment
|
140
|
+
@previous = @next
|
141
|
+
|
142
|
+
return finished unless @next = calculate_next(@previous)
|
143
|
+
return finished if @times && @count >= @times
|
144
|
+
return finished if @stop && @next > @stop
|
145
|
+
@next
|
146
|
+
end
|
147
|
+
|
148
|
+
# Will reinitalize the Scheduled event with start time beeing
|
149
|
+
# the current time. This results in seconds_left beeing the next
|
150
|
+
# positive amount (or nil if not postpone_stop and next event would
|
151
|
+
# be after @stop)
|
152
|
+
# Useful together with Scheduled#off, Scheduled#on
|
153
|
+
def reschedule(by_seconds=0, postpone_stop=true)
|
154
|
+
raise "Cannot postpone finished events." if @finished
|
155
|
+
@next = calculate_first(Time.now+by_seconds)
|
156
|
+
finished if !@next || (!postpone_stop && @stop && @next > @stop)
|
157
|
+
|
158
|
+
update_scheduler
|
159
|
+
@next
|
160
|
+
end
|
161
|
+
alias postpone reschedule
|
162
|
+
|
163
|
+
# alterates the next-time exactly by "by_seconds",
|
164
|
+
# doesn't influence anything else.
|
165
|
+
def alter(by_seconds=0)
|
166
|
+
@next += by_seconds
|
167
|
+
update_scheduler
|
168
|
+
@next
|
169
|
+
end
|
170
|
+
|
171
|
+
# Switch the Scheduled event on.
|
172
|
+
# seconds_left will return seconds again.
|
173
|
+
# See Scheduled#off, Scheduled#on?, Scheduled#off?
|
174
|
+
def on
|
175
|
+
@on = true
|
176
|
+
update_scheduler
|
177
|
+
self
|
178
|
+
end
|
179
|
+
|
180
|
+
# Switch the Scheduled event off.
|
181
|
+
# seconds_left will return nil.
|
182
|
+
# See Scheduled#on, Scheduled#on?, Scheduled#off?
|
183
|
+
def off
|
184
|
+
@on = false
|
185
|
+
update_scheduler
|
186
|
+
self
|
187
|
+
end
|
188
|
+
|
189
|
+
# Define the Event as finished and remove it from the scheduler (if scheduled).
|
190
|
+
# seconds_left will return nil, <=> will be 1 or 0 (0 for other finished events)
|
191
|
+
def finished
|
192
|
+
@finished = true
|
193
|
+
@wakeup.each { |thread| thread.wakeup }
|
194
|
+
nil
|
195
|
+
end
|
196
|
+
|
197
|
+
# returns seconds left until next Timed
|
198
|
+
# returns nil if event is past @stop-datetime
|
199
|
+
# returns nil if event has been processed the specified amount of times
|
200
|
+
def seconds_left(time=nil)
|
201
|
+
(!@finished && @on && @next) && @next - (time||Time.now)
|
202
|
+
end
|
203
|
+
|
204
|
+
# returns how many times the event still has to be executed
|
205
|
+
def times_left
|
206
|
+
@times && @times-@count
|
207
|
+
end
|
208
|
+
|
209
|
+
# Whether Scheduled event is due now.
|
210
|
+
# A finished or off? event is never due?.
|
211
|
+
def due?(time=nil)
|
212
|
+
@on && !@finished && seconds_left(time) <= 0
|
213
|
+
end
|
214
|
+
|
215
|
+
# Whether an event has been declared as finished or not.
|
216
|
+
# Events that have any kind of limit (:stop, :times) can
|
217
|
+
# become automatically finished.
|
218
|
+
# Finished events are never due?.
|
219
|
+
def finished?
|
220
|
+
@finished
|
221
|
+
end
|
222
|
+
|
223
|
+
# Whether an event has been switched on (default).
|
224
|
+
def on?
|
225
|
+
@on
|
226
|
+
end
|
227
|
+
|
228
|
+
# Whether an event has been switched off (see Scheduled#off).
|
229
|
+
# Off? events are never due?.
|
230
|
+
def off?
|
231
|
+
!@on
|
232
|
+
end
|
233
|
+
|
234
|
+
|
235
|
+
# returns -1, 0 or 1 (smaller, equal to, bigger then other)
|
236
|
+
# nil-values for seconds_left are treaded like +INF
|
237
|
+
# <=> is overloaded, returning for
|
238
|
+
# * Event: self.seconds_left <=> other.seconds_left (anything responding to .seconds_left)
|
239
|
+
# * Time: self.next <=> other (only instances of Time, not Date nor DateTime)
|
240
|
+
# * Numeric: self.to_f <=> other.to_f (anything responding to .to_f)
|
241
|
+
def <=>(other)
|
242
|
+
return @next ? @next <=> other : 1 if other.kind_of?(Time)
|
243
|
+
if other.respond_to?(:seconds_left)
|
244
|
+
me = seconds_left
|
245
|
+
other = other.seconds_left
|
246
|
+
else
|
247
|
+
me = to_f
|
248
|
+
other = other.to_f
|
249
|
+
end
|
250
|
+
return me <=> other if me and other
|
251
|
+
return -1 if me
|
252
|
+
return 1 if other
|
253
|
+
0
|
254
|
+
end
|
255
|
+
|
256
|
+
# The left time to next due? as integer.
|
257
|
+
# IGNORES ON/OFF STATUS, use seconds_left for that.
|
258
|
+
def to_i
|
259
|
+
seconds_left.to_i
|
260
|
+
end
|
261
|
+
|
262
|
+
# The left time to next due? as float.
|
263
|
+
# IGNORES ON/OFF STATUS, use seconds_left for that.
|
264
|
+
def to_f
|
265
|
+
seconds_left.to_f
|
266
|
+
end
|
267
|
+
|
268
|
+
def join
|
269
|
+
@wakeup << Thread.current
|
270
|
+
sleep unless finished?
|
271
|
+
end
|
272
|
+
|
273
|
+
# Normalizes inputs like "*", "1/12", 12, [1, 13], (1..5)
|
274
|
+
# to an array of all corresponding integers.
|
275
|
+
def extract_time_range(range, rangeEnd, divisors)
|
276
|
+
if range == "*" then
|
277
|
+
(0..rangeEnd).to_a
|
278
|
+
elsif (range.kind_of?(Integer) && divisors.include?(range)) then
|
279
|
+
(0..rangeEnd).stepped(range)
|
280
|
+
elsif (range.kind_of?(String) && !/^\d+\/\d+$/.match(range).nil?) then
|
281
|
+
a,b = *range.match(/(\d+)\/(\d+)/).captures
|
282
|
+
(a.to_i..rangeEnd).stepped(b.to_i)
|
283
|
+
elsif (range.kind_of?(Range) && range.begin >= 0 && range.end <= rangeEnd)
|
284
|
+
return range.to_a
|
285
|
+
elsif (range.kind_of?(Array))
|
286
|
+
return range
|
287
|
+
else
|
288
|
+
raise ArgumentError, "Invalid range #{range} (#{rangeEnd})"
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def update_scheduler
|
293
|
+
@scheduler.reschedule(self) if @scheduler
|
294
|
+
end
|
295
|
+
end
|