butler 1.8.0

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