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.
Files changed (164) hide show
  1. data/CHANGELOG +4 -0
  2. data/GPL.txt +340 -0
  3. data/LICENSE.txt +52 -0
  4. data/README +37 -0
  5. data/Rakefile +334 -0
  6. data/bin/botcontrol +230 -0
  7. data/data/butler/config_template.yaml +4 -0
  8. data/data/butler/dialogs/backup.rb +19 -0
  9. data/data/butler/dialogs/botcontrol.rb +4 -0
  10. data/data/butler/dialogs/config.rb +1 -0
  11. data/data/butler/dialogs/create.rb +53 -0
  12. data/data/butler/dialogs/delete.rb +3 -0
  13. data/data/butler/dialogs/en/backup.yaml +6 -0
  14. data/data/butler/dialogs/en/botcontrol.yaml +5 -0
  15. data/data/butler/dialogs/en/create.yaml +11 -0
  16. data/data/butler/dialogs/en/delete.yaml +2 -0
  17. data/data/butler/dialogs/en/help.yaml +17 -0
  18. data/data/butler/dialogs/en/info.yaml +13 -0
  19. data/data/butler/dialogs/en/list.yaml +4 -0
  20. data/data/butler/dialogs/en/notyetimplemented.yaml +2 -0
  21. data/data/butler/dialogs/en/rename.yaml +3 -0
  22. data/data/butler/dialogs/en/start.yaml +3 -0
  23. data/data/butler/dialogs/en/sync_plugins.yaml +3 -0
  24. data/data/butler/dialogs/en/uninstall.yaml +5 -0
  25. data/data/butler/dialogs/en/unknown_command.yaml +2 -0
  26. data/data/butler/dialogs/help.rb +11 -0
  27. data/data/butler/dialogs/info.rb +27 -0
  28. data/data/butler/dialogs/interactive.rb +1 -0
  29. data/data/butler/dialogs/list.rb +10 -0
  30. data/data/butler/dialogs/notyetimplemented.rb +1 -0
  31. data/data/butler/dialogs/rename.rb +4 -0
  32. data/data/butler/dialogs/selectbot.rb +2 -0
  33. data/data/butler/dialogs/start.rb +5 -0
  34. data/data/butler/dialogs/sync_plugins.rb +30 -0
  35. data/data/butler/dialogs/uninstall.rb +17 -0
  36. data/data/butler/dialogs/unknown_command.rb +1 -0
  37. data/data/butler/plugins/core/logout.rb +41 -0
  38. data/data/butler/plugins/core/plugins.rb +134 -0
  39. data/data/butler/plugins/core/privilege.rb +103 -0
  40. data/data/butler/plugins/core/user.rb +166 -0
  41. data/data/butler/plugins/dev/eval.rb +64 -0
  42. data/data/butler/plugins/dev/nometa.rb +14 -0
  43. data/data/butler/plugins/dev/onhandlers.rb +93 -0
  44. data/data/butler/plugins/dev/raw.rb +36 -0
  45. data/data/butler/plugins/dev/rawlog.rb +77 -0
  46. data/data/butler/plugins/games/eightball.rb +54 -0
  47. data/data/butler/plugins/games/mastermind.rb +174 -0
  48. data/data/butler/plugins/irc/action.rb +36 -0
  49. data/data/butler/plugins/irc/join.rb +38 -0
  50. data/data/butler/plugins/irc/notice.rb +36 -0
  51. data/data/butler/plugins/irc/part.rb +38 -0
  52. data/data/butler/plugins/irc/privmsg.rb +36 -0
  53. data/data/butler/plugins/irc/quit.rb +36 -0
  54. data/data/butler/plugins/operator/deop.rb +41 -0
  55. data/data/butler/plugins/operator/devoice.rb +41 -0
  56. data/data/butler/plugins/operator/limit.rb +47 -0
  57. data/data/butler/plugins/operator/op.rb +41 -0
  58. data/data/butler/plugins/operator/voice.rb +41 -0
  59. data/data/butler/plugins/public/help.rb +69 -0
  60. data/data/butler/plugins/public/login.rb +72 -0
  61. data/data/butler/plugins/public/usage.rb +49 -0
  62. data/data/butler/plugins/service/clones.rb +56 -0
  63. data/data/butler/plugins/service/define.rb +47 -0
  64. data/data/butler/plugins/service/log.rb +183 -0
  65. data/data/butler/plugins/service/svn.rb +91 -0
  66. data/data/butler/plugins/util/cycle.rb +98 -0
  67. data/data/butler/plugins/util/load.rb +41 -0
  68. data/data/butler/plugins/util/pong.rb +29 -0
  69. data/data/butler/strings/random/acknowledge.en.yaml +5 -0
  70. data/data/butler/strings/random/gratitude.en.yaml +3 -0
  71. data/data/butler/strings/random/hello.en.yaml +4 -0
  72. data/data/butler/strings/random/ignorance.en.yaml +7 -0
  73. data/data/butler/strings/random/ignorance_about.en.yaml +3 -0
  74. data/data/butler/strings/random/insult.en.yaml +3 -0
  75. data/data/butler/strings/random/rejection.en.yaml +12 -0
  76. data/data/man/botcontrol.1 +17 -0
  77. data/lib/access.rb +187 -0
  78. data/lib/access/admin.rb +16 -0
  79. data/lib/access/privilege.rb +122 -0
  80. data/lib/access/role.rb +102 -0
  81. data/lib/access/savable.rb +18 -0
  82. data/lib/access/user.rb +180 -0
  83. data/lib/access/yamlbase.rb +126 -0
  84. data/lib/butler.rb +188 -0
  85. data/lib/butler/bot.rb +247 -0
  86. data/lib/butler/control.rb +93 -0
  87. data/lib/butler/dialog.rb +64 -0
  88. data/lib/butler/initialvalues.rb +40 -0
  89. data/lib/butler/irc/channel.rb +135 -0
  90. data/lib/butler/irc/channels.rb +96 -0
  91. data/lib/butler/irc/client.rb +351 -0
  92. data/lib/butler/irc/hostmask.rb +53 -0
  93. data/lib/butler/irc/message.rb +184 -0
  94. data/lib/butler/irc/parser.rb +125 -0
  95. data/lib/butler/irc/parser/commands.rb +83 -0
  96. data/lib/butler/irc/parser/generic.rb +343 -0
  97. data/lib/butler/irc/socket.rb +378 -0
  98. data/lib/butler/irc/string.rb +186 -0
  99. data/lib/butler/irc/topic.rb +15 -0
  100. data/lib/butler/irc/user.rb +265 -0
  101. data/lib/butler/irc/users.rb +112 -0
  102. data/lib/butler/plugin.rb +249 -0
  103. data/lib/butler/plugin/configproxy.rb +35 -0
  104. data/lib/butler/plugin/mapper.rb +85 -0
  105. data/lib/butler/plugin/matcher.rb +55 -0
  106. data/lib/butler/plugin/onhandlers.rb +70 -0
  107. data/lib/butler/plugin/trigger.rb +58 -0
  108. data/lib/butler/plugins.rb +147 -0
  109. data/lib/butler/version.rb +17 -0
  110. data/lib/cloptions.rb +217 -0
  111. data/lib/cloptions/adapters.rb +24 -0
  112. data/lib/cloptions/switch.rb +132 -0
  113. data/lib/configuration.rb +223 -0
  114. data/lib/dialogline.rb +296 -0
  115. data/lib/dialogline/localizations.rb +24 -0
  116. data/lib/durations.rb +57 -0
  117. data/lib/event.rb +295 -0
  118. data/lib/event/at.rb +64 -0
  119. data/lib/event/every.rb +56 -0
  120. data/lib/event/timed.rb +112 -0
  121. data/lib/installer.rb +75 -0
  122. data/lib/iterator.rb +34 -0
  123. data/lib/log.rb +68 -0
  124. data/lib/log/comfort.rb +85 -0
  125. data/lib/log/converter.rb +23 -0
  126. data/lib/log/entry.rb +152 -0
  127. data/lib/log/fakeio.rb +55 -0
  128. data/lib/log/file.rb +54 -0
  129. data/lib/log/filereader.rb +81 -0
  130. data/lib/log/forward.rb +49 -0
  131. data/lib/log/methods.rb +39 -0
  132. data/lib/log/nolog.rb +18 -0
  133. data/lib/log/splitter.rb +26 -0
  134. data/lib/ostructfixed.rb +26 -0
  135. data/lib/ruby/array/columnize.rb +38 -0
  136. data/lib/ruby/dir/mktree.rb +28 -0
  137. data/lib/ruby/enumerable/join.rb +13 -0
  138. data/lib/ruby/exception/detailed.rb +24 -0
  139. data/lib/ruby/file/append.rb +11 -0
  140. data/lib/ruby/file/write.rb +11 -0
  141. data/lib/ruby/hash/zip.rb +15 -0
  142. data/lib/ruby/kernel/bench.rb +15 -0
  143. data/lib/ruby/kernel/daemonize.rb +42 -0
  144. data/lib/ruby/kernel/non_verbose.rb +17 -0
  145. data/lib/ruby/kernel/safe_fork.rb +18 -0
  146. data/lib/ruby/range/stepped.rb +11 -0
  147. data/lib/ruby/string/arguments.rb +72 -0
  148. data/lib/ruby/string/chunks.rb +15 -0
  149. data/lib/ruby/string/post_arguments.rb +44 -0
  150. data/lib/ruby/string/unescaped.rb +17 -0
  151. data/lib/scheduler.rb +164 -0
  152. data/lib/scriptfile.rb +101 -0
  153. data/lib/templater.rb +86 -0
  154. data/test/cloptions.rb +134 -0
  155. data/test/cv.rb +28 -0
  156. data/test/irc/client.rb +85 -0
  157. data/test/irc/client_login.txt +53 -0
  158. data/test/irc/client_subscribe.txt +8 -0
  159. data/test/irc/message.rb +30 -0
  160. data/test/irc/messages.txt +64 -0
  161. data/test/irc/parser.rb +13 -0
  162. data/test/irc/profile_parser.rb +12 -0
  163. data/test/irc/users.rb +28 -0
  164. metadata +256 -0
@@ -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
@@ -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
@@ -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