butler 1.8.0
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 +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
|