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