cakewalk 3.0.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 (99) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +1 -0
  3. data/LICENSE +22 -0
  4. data/README.md +169 -0
  5. data/docs/bot_options.md +454 -0
  6. data/docs/changes.md +541 -0
  7. data/docs/common_mistakes.md +60 -0
  8. data/docs/common_tasks.md +57 -0
  9. data/docs/encodings.md +69 -0
  10. data/docs/events.md +273 -0
  11. data/docs/getting_started.md +184 -0
  12. data/docs/logging.md +90 -0
  13. data/docs/migrating.md +267 -0
  14. data/docs/plugins.md +4 -0
  15. data/docs/readme.md +20 -0
  16. data/examples/basic/autovoice.rb +32 -0
  17. data/examples/basic/google.rb +35 -0
  18. data/examples/basic/hello.rb +15 -0
  19. data/examples/basic/join_part.rb +34 -0
  20. data/examples/basic/memo.rb +39 -0
  21. data/examples/basic/msg.rb +16 -0
  22. data/examples/basic/seen.rb +36 -0
  23. data/examples/basic/urban_dict.rb +35 -0
  24. data/examples/basic/url_shorten.rb +35 -0
  25. data/examples/plugins/autovoice.rb +37 -0
  26. data/examples/plugins/custom_prefix.rb +23 -0
  27. data/examples/plugins/dice_roll.rb +38 -0
  28. data/examples/plugins/google.rb +36 -0
  29. data/examples/plugins/hello.rb +22 -0
  30. data/examples/plugins/hooks.rb +36 -0
  31. data/examples/plugins/join_part.rb +42 -0
  32. data/examples/plugins/lambdas.rb +35 -0
  33. data/examples/plugins/last_nick.rb +24 -0
  34. data/examples/plugins/msg.rb +22 -0
  35. data/examples/plugins/multiple_matches.rb +32 -0
  36. data/examples/plugins/own_events.rb +37 -0
  37. data/examples/plugins/seen.rb +45 -0
  38. data/examples/plugins/timer.rb +22 -0
  39. data/examples/plugins/url_shorten.rb +33 -0
  40. data/lib/cakewalk/ban.rb +50 -0
  41. data/lib/cakewalk/bot.rb +479 -0
  42. data/lib/cakewalk/cached_list.rb +19 -0
  43. data/lib/cakewalk/callback.rb +20 -0
  44. data/lib/cakewalk/channel.rb +463 -0
  45. data/lib/cakewalk/channel_list.rb +29 -0
  46. data/lib/cakewalk/configuration/bot.rb +48 -0
  47. data/lib/cakewalk/configuration/dcc.rb +16 -0
  48. data/lib/cakewalk/configuration/plugins.rb +41 -0
  49. data/lib/cakewalk/configuration/sasl.rb +19 -0
  50. data/lib/cakewalk/configuration/ssl.rb +19 -0
  51. data/lib/cakewalk/configuration/timeouts.rb +14 -0
  52. data/lib/cakewalk/configuration.rb +73 -0
  53. data/lib/cakewalk/constants.rb +533 -0
  54. data/lib/cakewalk/dcc/dccable_object.rb +37 -0
  55. data/lib/cakewalk/dcc/incoming/send.rb +147 -0
  56. data/lib/cakewalk/dcc/incoming.rb +1 -0
  57. data/lib/cakewalk/dcc/outgoing/send.rb +122 -0
  58. data/lib/cakewalk/dcc/outgoing.rb +1 -0
  59. data/lib/cakewalk/dcc.rb +12 -0
  60. data/lib/cakewalk/exceptions.rb +46 -0
  61. data/lib/cakewalk/formatting.rb +125 -0
  62. data/lib/cakewalk/handler.rb +118 -0
  63. data/lib/cakewalk/handler_list.rb +90 -0
  64. data/lib/cakewalk/helpers.rb +231 -0
  65. data/lib/cakewalk/irc.rb +913 -0
  66. data/lib/cakewalk/isupport.rb +98 -0
  67. data/lib/cakewalk/log_filter.rb +21 -0
  68. data/lib/cakewalk/logger/formatted_logger.rb +97 -0
  69. data/lib/cakewalk/logger/zcbot_logger.rb +22 -0
  70. data/lib/cakewalk/logger.rb +168 -0
  71. data/lib/cakewalk/logger_list.rb +85 -0
  72. data/lib/cakewalk/mask.rb +69 -0
  73. data/lib/cakewalk/message.rb +391 -0
  74. data/lib/cakewalk/message_queue.rb +107 -0
  75. data/lib/cakewalk/mode_parser.rb +76 -0
  76. data/lib/cakewalk/network.rb +89 -0
  77. data/lib/cakewalk/open_ended_queue.rb +26 -0
  78. data/lib/cakewalk/pattern.rb +65 -0
  79. data/lib/cakewalk/plugin.rb +515 -0
  80. data/lib/cakewalk/plugin_list.rb +38 -0
  81. data/lib/cakewalk/rubyext/float.rb +3 -0
  82. data/lib/cakewalk/rubyext/module.rb +26 -0
  83. data/lib/cakewalk/rubyext/string.rb +33 -0
  84. data/lib/cakewalk/sasl/dh_blowfish.rb +71 -0
  85. data/lib/cakewalk/sasl/diffie_hellman.rb +47 -0
  86. data/lib/cakewalk/sasl/mechanism.rb +6 -0
  87. data/lib/cakewalk/sasl/plain.rb +26 -0
  88. data/lib/cakewalk/sasl.rb +34 -0
  89. data/lib/cakewalk/syncable.rb +83 -0
  90. data/lib/cakewalk/target.rb +199 -0
  91. data/lib/cakewalk/timer.rb +145 -0
  92. data/lib/cakewalk/user.rb +488 -0
  93. data/lib/cakewalk/user_list.rb +87 -0
  94. data/lib/cakewalk/utilities/deprecation.rb +16 -0
  95. data/lib/cakewalk/utilities/encoding.rb +37 -0
  96. data/lib/cakewalk/utilities/kernel.rb +13 -0
  97. data/lib/cakewalk/version.rb +4 -0
  98. data/lib/cakewalk.rb +5 -0
  99. metadata +140 -0
@@ -0,0 +1,71 @@
1
+ require "openssl"
2
+ require "base64"
3
+ require "cakewalk/sasl/mechanism"
4
+
5
+ module Cakewalk
6
+ module SASL
7
+ # DH-BLOWFISH is a combination of Diffie-Hellman key exchange and
8
+ # the Blowfish encryption algorithm. Due to its nature it is more
9
+ # secure than transmitting the password unencrypted and can be
10
+ # used on potentially insecure networks.
11
+ class DH_Blowfish < Mechanism
12
+ class << self
13
+ # @return [String]
14
+ def mechanism_name
15
+ "DH-BLOWFISH"
16
+ end
17
+
18
+ # @return [Array(Numeric, Numeric, Numeric)] p, g and y for DH
19
+ def unpack_payload(payload)
20
+ pgy = []
21
+ payload = payload.dup
22
+
23
+ 3.times do
24
+ size = payload.unpack("n").first
25
+ payload.slice!(0, 2)
26
+ pgy << payload.unpack("a#{size}").first
27
+ payload.slice!(0, size)
28
+ end
29
+
30
+ pgy.map {|i| OpenSSL::BN.new(i, 2).to_i}
31
+ end
32
+
33
+ # @param [String] user
34
+ # @param [String] password
35
+ # @param [String] payload
36
+ # @return [String]
37
+ def generate(user, password, payload)
38
+ # duplicate the passed strings because we are modifying them
39
+ # later and they might come from the configuration store or
40
+ # similar
41
+ user = user.dup
42
+ password = password.dup
43
+
44
+ data = Base64.decode64(payload).force_encoding("ASCII-8BIT")
45
+
46
+ p, g, y = unpack_payload(data)
47
+
48
+ dh = DiffieHellman.new(p, g, 23)
49
+ pub_key = dh.generate
50
+ secret = OpenSSL::BN.new(dh.secret(y).to_s).to_s(2)
51
+ public = OpenSSL::BN.new(pub_key.to_s).to_s(2)
52
+
53
+ # Pad password so its length is a multiple of the cipher block size
54
+ password << "\0"
55
+ password << "." * (8 - (password.size % 8))
56
+
57
+ crypted = ""
58
+ cipher = OpenSSL::Cipher.new("BF-ECB")
59
+ cipher.key_len = 32 # OpenSSL's default of 16 doesn't work
60
+ cipher.encrypt
61
+ cipher.key = secret
62
+
63
+ crypted = cipher.update(password) # we do not want the content of cipher.final
64
+
65
+ answer = [public.bytesize, public, user, crypted].pack("na*Z*a*")
66
+ Base64.strict_encode64(answer)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,47 @@
1
+ module Cakewalk
2
+ module SASL
3
+ class DiffieHellman
4
+ attr_reader :p, :g, :q, :x, :e
5
+
6
+ def initialize(p, g, q)
7
+ @p = p
8
+ @g = g
9
+ @q = q
10
+ end
11
+
12
+ def generate(tries = 16)
13
+ tries.times do
14
+ @x = rand(@q)
15
+ @e = mod_exp(@g, @x, @p)
16
+ return @e if valid?
17
+ end
18
+ raise ArgumentError, "can't generate valid e"
19
+ end
20
+
21
+ # compute the shared secret, given the public key
22
+ def secret(f)
23
+ mod_exp(f, @x, @p)
24
+ end
25
+
26
+ private
27
+ # validate a public key
28
+ def valid?
29
+ @e && @e.between?(2, @p - 2) && bits_set(@e) > 1
30
+ end
31
+
32
+ def bits_set(e)
33
+ ("%b" % e).count('1')
34
+ end
35
+
36
+ def mod_exp(b, e, m)
37
+ result = 1
38
+ while e > 0
39
+ result = (result * b) % m if e[0] == 1
40
+ e = e >> 1
41
+ b = (b * b) % m
42
+ end
43
+ return result
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,6 @@
1
+ module Cakewalk
2
+ module SASL
3
+ class Mechanism
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,26 @@
1
+ require "base64"
2
+ require "cakewalk/sasl/mechanism"
3
+
4
+ module Cakewalk
5
+ module SASL
6
+ # The simplest mechanisms simply transmits the username and
7
+ # password without adding any encryption or hashing. As such it's more
8
+ # insecure than DH-BLOWFISH and should only be used in combination with
9
+ # SSL.
10
+ class Plain < Mechanism
11
+ class << self
12
+ # @return [String]
13
+ def mechanism_name
14
+ "PLAIN"
15
+ end
16
+
17
+ # @param [String] user
18
+ # @param [String] password
19
+ # @return [String]
20
+ def generate(user, password, _ = nil)
21
+ Base64.strict_encode64([user, user, password].join("\0"))
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,34 @@
1
+ require "cakewalk/sasl/diffie_hellman"
2
+ require "cakewalk/sasl/plain"
3
+ require "cakewalk/sasl/dh_blowfish"
4
+
5
+ module Cakewalk
6
+ # SASL is a modern way of authentication in IRC, solving problems
7
+ # such as transmitting passwords as plain text (see the DH-BLOWFISH
8
+ # mechanism) and fully identifying before joining any channels.
9
+ #
10
+ # Cakewalk automatically detects which mechanisms are supported by the
11
+ # IRC network and uses the best available one.
12
+ #
13
+ # # Supported Mechanisms
14
+ #
15
+ # - {SASL::DH_Blowfish DH-BLOWFISH}
16
+ # - {SASL::Plain PLAIN}
17
+ #
18
+ # # Configuration
19
+ # In order to use SASL one has to set the username and password
20
+ # options as follows:
21
+ #
22
+ # configure do |c|
23
+ # c.sasl.username = "foo"
24
+ # c.sasl.password = "bar"
25
+ # end
26
+ #
27
+ # @note All classes and modules in this module are for internal use by
28
+ # Cakewalk only.
29
+ #
30
+ # @api private
31
+ # @since 2.0.0
32
+ module SASL
33
+ end
34
+ end
@@ -0,0 +1,83 @@
1
+ module Cakewalk
2
+ # Provide blocking access to user/channel information.
3
+ module Syncable
4
+ # Blocks until the object is synced.
5
+ #
6
+ # @return [void]
7
+ # @api private
8
+ def wait_until_synced(attr)
9
+ attr = attr.to_sym
10
+ waited = 0
11
+ while true
12
+ return if attribute_synced?(attr)
13
+ waited += 1
14
+
15
+ if waited % 100 == 0
16
+ bot.loggers.warn "A synced attribute ('%s' for %s) has not been available for %d seconds, still waiting" % [attr, self.inspect, waited / 10]
17
+ bot.loggers.warn caller.map {|s| " #{s}"}
18
+
19
+ if waited / 10 >= 30
20
+ bot.loggers.warn " Giving up..."
21
+ raise Exceptions::SyncedAttributeNotAvailable, "'%s' for %s" % [attr, self.inspect]
22
+ end
23
+ end
24
+ sleep 0.1
25
+ end
26
+ end
27
+
28
+ # @api private
29
+ # @return [void]
30
+ def sync(attribute, value, data = false)
31
+ if data
32
+ @data[attribute] = value
33
+ else
34
+ instance_variable_set("@#{attribute}", value)
35
+ end
36
+ @synced_attributes << attribute
37
+ end
38
+
39
+ # @return [Boolean]
40
+ # @api private
41
+ def attribute_synced?(attribute)
42
+ @synced_attributes.include?(attribute)
43
+ end
44
+
45
+ # @return [void]
46
+ # @api private
47
+ def unsync(attribute)
48
+ @synced_attributes.delete(attribute)
49
+ end
50
+
51
+ # @return [void]
52
+ # @api private
53
+ # @since 1.0.1
54
+ def unsync_all
55
+ @synced_attributes.clear
56
+ end
57
+
58
+ # @param [Symbol] attribute
59
+ # @param [Boolean] data
60
+ # @param [Boolean] unsync
61
+ # @api private
62
+ def attr(attribute, data = false, unsync = false)
63
+ unless unsync
64
+ if @when_requesting_synced_attribute
65
+ @when_requesting_synced_attribute.call(attribute)
66
+ end
67
+ wait_until_synced(attribute)
68
+ end
69
+
70
+ if data
71
+ return @data[attribute]
72
+ else
73
+ return instance_variable_get("@#{attribute}")
74
+ end
75
+ end
76
+
77
+ # @api private
78
+ # @return [void]
79
+ def mark_as_synced(attribute)
80
+ @synced_attributes << attribute
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,199 @@
1
+ module Cakewalk
2
+ # @since 2.0.0
3
+ class Target
4
+ include Comparable
5
+
6
+ # @return [String]
7
+ attr_reader :name
8
+ # @return [Bot]
9
+ attr_reader :bot
10
+ def initialize(name, bot)
11
+ @name = name
12
+ @bot = bot
13
+ end
14
+
15
+ # Sends a NOTICE to the target.
16
+ #
17
+ # @param [#to_s] text the message to send
18
+ # @return [void]
19
+ # @see #safe_notice
20
+ def notice(text)
21
+ send(text, true)
22
+ end
23
+
24
+ # Sends a PRIVMSG to the target.
25
+ #
26
+ # @param [#to_s] text the message to send
27
+ # @param [Boolean] notice Use NOTICE instead of PRIVMSG?
28
+ # @return [void]
29
+ # @see #safe_msg
30
+ # @note The aliases `msg` and `privmsg` are deprecated and will be
31
+ # removed in a future version.
32
+ def send(text, notice = false)
33
+ # TODO deprecate `notice` argument, put splitting into own
34
+ # method
35
+ text = text.to_s
36
+ split_start = @bot.config.message_split_start || ""
37
+ split_end = @bot.config.message_split_end || ""
38
+ command = notice ? "NOTICE" : "PRIVMSG"
39
+ prefix = ":#{@bot.mask} #{command} #{@name} :"
40
+
41
+ text.lines.map(&:chomp).each do |line|
42
+ splitted = split_message(line, prefix, split_start, split_end)
43
+
44
+ splitted[0, (@bot.config.max_messages || splitted.size)].each do |string|
45
+ @bot.irc.send("#{command} #@name :#{string}")
46
+ end
47
+ end
48
+ end
49
+ alias_method :msg, :send # deprecated
50
+ alias_method :privmsg, :send # deprecated
51
+ undef_method(:msg) # yardoc hack
52
+ undef_method(:privmsg) # yardoc hack
53
+
54
+ # @deprecated
55
+ def msg(*args)
56
+ Cakewalk::Utilities::Deprecation.print_deprecation("2.2.0", "Target#msg", "Target#send")
57
+ send(*args)
58
+ end
59
+
60
+ # @deprecated
61
+ def privmsg(*args)
62
+ Cakewalk::Utilities::Deprecation.print_deprecation("2.2.0", "Target#privmsg", "Target#send")
63
+ send(*args)
64
+ end
65
+
66
+ # Like {#send}, but remove any non-printable characters from
67
+ # `text`. The purpose of this method is to send text of untrusted
68
+ # sources, like other users or feeds.
69
+ #
70
+ # Note: this will **break** any mIRC color codes embedded in the
71
+ # string. For more fine-grained control, use
72
+ # {Helpers#Sanitize} and
73
+ # {Formatting.unformat} directly.
74
+ #
75
+ # @return (see #send)
76
+ # @param (see #send)
77
+ # @see #send
78
+ def safe_send(text, notice = false)
79
+ send(Cakewalk::Helpers.sanitize(text), notice)
80
+ end
81
+ alias_method :safe_msg, :safe_send # deprecated
82
+ alias_method :safe_privmsg, :safe_msg # deprecated
83
+ undef_method(:safe_msg) # yardoc hack
84
+ undef_method(:safe_privmsg) # yardoc hack
85
+
86
+ # @deprecated
87
+ def safe_msg(*args)
88
+ Cakewalk::Utilities::Deprecation.print_deprecation("2.2.0", "Target#safe_msg", "Target#safe_send")
89
+ send(*args)
90
+ end
91
+
92
+ # @deprecated
93
+ def safe_privmsg(*args)
94
+ Cakewalk::Utilities::Deprecation.print_deprecation("2.2.0", "Target#safe_privmsg", "Target#safe_send")
95
+ send(*args)
96
+ end
97
+
98
+
99
+ # Like {#safe_msg} but for notices.
100
+ #
101
+ # @return (see #safe_msg)
102
+ # @param (see #safe_msg)
103
+ # @see #safe_notice
104
+ # @see #notice
105
+ def safe_notice(text)
106
+ safe_send(text, true)
107
+ end
108
+
109
+ # Invoke an action (/me) in/to the target.
110
+ #
111
+ # @param [#to_s] text the message to send
112
+ # @return [void]
113
+ # @see #safe_action
114
+ def action(text)
115
+ line = text.to_s.each_line.first.chomp
116
+ @bot.irc.send("PRIVMSG #@name :\001ACTION #{line}\001")
117
+ end
118
+
119
+ # Like {#action}, but remove any non-printable characters from
120
+ # `text`. The purpose of this method is to send text from
121
+ # untrusted sources, like other users or feeds.
122
+ #
123
+ # Note: this will **break** any mIRC color codes embedded in the
124
+ # string. For more fine-grained control, use
125
+ # {Helpers#Sanitize} and
126
+ # {Formatting.unformat} directly.
127
+ #
128
+ # @param (see #action)
129
+ # @return (see #action)
130
+ # @see #action
131
+ def safe_action(text)
132
+ action(Cakewalk::Helpers.sanitize(text))
133
+ end
134
+
135
+ # Send a CTCP to the target.
136
+ #
137
+ # @param [#to_s] message the ctcp message
138
+ # @return [void]
139
+ def ctcp(message)
140
+ send "\001#{message}\001"
141
+ end
142
+
143
+ def concretize
144
+ if @bot.isupport["CHANTYPES"].include?(@name[0])
145
+ @bot.channel_list.find_ensured(@name)
146
+ else
147
+ @bot.user_list.find_ensured(@name)
148
+ end
149
+ end
150
+
151
+ # @return [Boolean]
152
+ def eql?(other)
153
+ self == other
154
+ end
155
+
156
+ # @param [Target, String] other
157
+ # @return [-1, 0, 1, nil]
158
+ def <=>(other)
159
+ casemapping = @bot.irc.isupport["CASEMAPPING"]
160
+ left = @name.irc_downcase(casemapping)
161
+
162
+ if other.is_a?(Target)
163
+ left <=> other.name.irc_downcase(casemapping)
164
+ elsif other.is_a?(String)
165
+ left <=> other.irc_downcase(casemapping)
166
+ else
167
+ nil
168
+ end
169
+ end
170
+
171
+ private
172
+ def split_message(msg, prefix, split_start, split_end)
173
+ max_bytesize = 510 - prefix.bytesize
174
+ max_bytesize_without_end = max_bytesize - split_end.bytesize
175
+
176
+ if msg.bytesize <= max_bytesize
177
+ return [msg]
178
+ end
179
+
180
+ splitted = []
181
+ while msg.bytesize > max_bytesize_without_end
182
+ acc = 0
183
+ acc_rune_sizes = msg.each_char.map {|ch|
184
+ acc += ch.bytesize
185
+ }
186
+
187
+ max_rune = acc_rune_sizes.rindex {|bs| bs <= max_bytesize_without_end} || 0
188
+ r = [msg.rindex(/\s/, max_rune) || (max_rune + 1), 1].max
189
+
190
+ splitted << (msg[0...r] + split_end)
191
+ msg = split_start.tr(" ", "\cz") + msg[r..-1].lstrip
192
+ end
193
+ splitted << msg
194
+
195
+ # clean string from any substitute characters
196
+ splitted.map {|string| string.tr("\cz", " ")}
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,145 @@
1
+ require "cakewalk/helpers"
2
+
3
+ module Cakewalk
4
+ # Timers are used for executing code in the future, either
5
+ # repeatedly or only once.
6
+ #
7
+ # In Cakewalk, two ways for creating timers are available:
8
+ #
9
+ # - The first way is by declaring them for a plugin, in which case
10
+ # they will start as soon as the bot connects to a server.
11
+ #
12
+ # - The second way is to dynamically create new timers in response
13
+ # to user input. A common example for this is an alarm clock
14
+ # plugin, which has to execute at a specific time.
15
+ #
16
+ # @see Helpers#Timer For dynamically creating timers
17
+ # @see Plugin::ClassMethods#timer For declaring timers in plugins
18
+ # @note It is possible to directly create instances of this class,
19
+ # but the referenced methods should suffice.
20
+ # @since 2.0.0
21
+ class Timer
22
+ include Helpers
23
+
24
+ # @return [Bot]
25
+ attr_reader :bot
26
+
27
+ # @return [Numeric] The interval (in seconds) of the timer
28
+ attr_accessor :interval
29
+
30
+ # @return [Boolean] If true, each invocation will be
31
+ # executed in a thread of its own.
32
+ attr_accessor :threaded
33
+
34
+ # @return [Proc]
35
+ attr_reader :block
36
+
37
+ # @return [Boolean]
38
+ attr_reader :started
39
+
40
+ # @return [Integer] The remaining number of shots before this timer
41
+ # will stop. This value will automatically reset after
42
+ # restarting the timer.
43
+ attr_accessor :shots
44
+ alias_method :threaded?, :threaded
45
+ alias_method :started?, :started
46
+
47
+ # @return [ThreadGroup]
48
+ # @api private
49
+ attr_reader :thread_group
50
+
51
+ # @param [Bot] bot The instance of {Bot} the timer is associated
52
+ # with
53
+ # @option options [Numeric] :interval The interval (in seconds) of
54
+ # the timer
55
+ # @option options [Integer] :shots (Float::INFINITY) How often should the
56
+ # timer fire?
57
+ # @option options [Boolean] :threaded (true) If true, each invocation will be
58
+ # executed in a thread of its own.
59
+ # @option options [Boolean] :start_automatically (true) If true,
60
+ # the timer will automatically start after the bot finished
61
+ # connecting.
62
+ # @option options [Boolean] :stop_automaticall (true) If true, the
63
+ # timer will automatically stop when the bot disconnects.
64
+ def initialize(bot, options, &block)
65
+ options = {:threaded => true, :shots => Float::INFINITY, :start_automatically => true, :stop_automatically => true}.merge(options)
66
+
67
+ @bot = bot
68
+ @interval = options[:interval].to_f
69
+ @threaded = options[:threaded]
70
+ @orig_shots = options[:shots]
71
+ # Setting @shots here so the attr_reader won't return nil
72
+ @shots = @orig_shots
73
+ @block = block
74
+
75
+ @started = false
76
+ @thread_group = ThreadGroup.new
77
+
78
+ if options[:start_automatically]
79
+ @bot.on :connect, //, self do |m, timer|
80
+ timer.start
81
+ end
82
+ end
83
+
84
+ if options[:stop_automatically]
85
+ @bot.on :disconnect, //, self do |m, timer|
86
+ timer.stop
87
+ end
88
+ end
89
+ end
90
+
91
+ # @return [Boolean]
92
+ def stopped?
93
+ !@started
94
+ end
95
+
96
+ # Start the timer
97
+ #
98
+ # @return [void]
99
+ def start
100
+ return if @started
101
+
102
+ @bot.loggers.debug "[timer] Starting timer #{self}"
103
+
104
+ @shots = @orig_shots
105
+
106
+ @thread_group.add Thread.new {
107
+ while @shots > 0 do
108
+ sleep @interval
109
+ if threaded?
110
+ Thread.new do
111
+ rescue_exception do
112
+ @block.call
113
+ end
114
+ end
115
+ else
116
+ rescue_exception do
117
+ @block.call
118
+ end
119
+ end
120
+
121
+ @shots -= 1
122
+ end
123
+ }
124
+
125
+ @started = true
126
+ end
127
+
128
+ # Stop the timer
129
+ #
130
+ # @return [void]
131
+ def stop
132
+ return unless @started
133
+
134
+ @bot.loggers.debug "[timer] Stopping timer #{self}"
135
+
136
+ @thread_group.list.each { |thread| thread.kill }
137
+ @started = false
138
+ end
139
+
140
+ # @return [String]
141
+ def to_s
142
+ "<Cakewalk::Timer %s/%s shots, %ds interval, %sthreaded, %sstarted, block: %s>" % [@orig_shots - @shots, @orig_shots, @interval, @threaded ? "" : "not ", @started ? "" : "not ", @block]
143
+ end
144
+ end
145
+ end