cakewalk 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.yardopts +1 -0
- data/LICENSE +22 -0
- data/README.md +169 -0
- data/docs/bot_options.md +454 -0
- data/docs/changes.md +541 -0
- data/docs/common_mistakes.md +60 -0
- data/docs/common_tasks.md +57 -0
- data/docs/encodings.md +69 -0
- data/docs/events.md +273 -0
- data/docs/getting_started.md +184 -0
- data/docs/logging.md +90 -0
- data/docs/migrating.md +267 -0
- data/docs/plugins.md +4 -0
- data/docs/readme.md +20 -0
- data/examples/basic/autovoice.rb +32 -0
- data/examples/basic/google.rb +35 -0
- data/examples/basic/hello.rb +15 -0
- data/examples/basic/join_part.rb +34 -0
- data/examples/basic/memo.rb +39 -0
- data/examples/basic/msg.rb +16 -0
- data/examples/basic/seen.rb +36 -0
- data/examples/basic/urban_dict.rb +35 -0
- data/examples/basic/url_shorten.rb +35 -0
- data/examples/plugins/autovoice.rb +37 -0
- data/examples/plugins/custom_prefix.rb +23 -0
- data/examples/plugins/dice_roll.rb +38 -0
- data/examples/plugins/google.rb +36 -0
- data/examples/plugins/hello.rb +22 -0
- data/examples/plugins/hooks.rb +36 -0
- data/examples/plugins/join_part.rb +42 -0
- data/examples/plugins/lambdas.rb +35 -0
- data/examples/plugins/last_nick.rb +24 -0
- data/examples/plugins/msg.rb +22 -0
- data/examples/plugins/multiple_matches.rb +32 -0
- data/examples/plugins/own_events.rb +37 -0
- data/examples/plugins/seen.rb +45 -0
- data/examples/plugins/timer.rb +22 -0
- data/examples/plugins/url_shorten.rb +33 -0
- data/lib/cakewalk/ban.rb +50 -0
- data/lib/cakewalk/bot.rb +479 -0
- data/lib/cakewalk/cached_list.rb +19 -0
- data/lib/cakewalk/callback.rb +20 -0
- data/lib/cakewalk/channel.rb +463 -0
- data/lib/cakewalk/channel_list.rb +29 -0
- data/lib/cakewalk/configuration/bot.rb +48 -0
- data/lib/cakewalk/configuration/dcc.rb +16 -0
- data/lib/cakewalk/configuration/plugins.rb +41 -0
- data/lib/cakewalk/configuration/sasl.rb +19 -0
- data/lib/cakewalk/configuration/ssl.rb +19 -0
- data/lib/cakewalk/configuration/timeouts.rb +14 -0
- data/lib/cakewalk/configuration.rb +73 -0
- data/lib/cakewalk/constants.rb +533 -0
- data/lib/cakewalk/dcc/dccable_object.rb +37 -0
- data/lib/cakewalk/dcc/incoming/send.rb +147 -0
- data/lib/cakewalk/dcc/incoming.rb +1 -0
- data/lib/cakewalk/dcc/outgoing/send.rb +122 -0
- data/lib/cakewalk/dcc/outgoing.rb +1 -0
- data/lib/cakewalk/dcc.rb +12 -0
- data/lib/cakewalk/exceptions.rb +46 -0
- data/lib/cakewalk/formatting.rb +125 -0
- data/lib/cakewalk/handler.rb +118 -0
- data/lib/cakewalk/handler_list.rb +90 -0
- data/lib/cakewalk/helpers.rb +231 -0
- data/lib/cakewalk/irc.rb +913 -0
- data/lib/cakewalk/isupport.rb +98 -0
- data/lib/cakewalk/log_filter.rb +21 -0
- data/lib/cakewalk/logger/formatted_logger.rb +97 -0
- data/lib/cakewalk/logger/zcbot_logger.rb +22 -0
- data/lib/cakewalk/logger.rb +168 -0
- data/lib/cakewalk/logger_list.rb +85 -0
- data/lib/cakewalk/mask.rb +69 -0
- data/lib/cakewalk/message.rb +391 -0
- data/lib/cakewalk/message_queue.rb +107 -0
- data/lib/cakewalk/mode_parser.rb +76 -0
- data/lib/cakewalk/network.rb +89 -0
- data/lib/cakewalk/open_ended_queue.rb +26 -0
- data/lib/cakewalk/pattern.rb +65 -0
- data/lib/cakewalk/plugin.rb +515 -0
- data/lib/cakewalk/plugin_list.rb +38 -0
- data/lib/cakewalk/rubyext/float.rb +3 -0
- data/lib/cakewalk/rubyext/module.rb +26 -0
- data/lib/cakewalk/rubyext/string.rb +33 -0
- data/lib/cakewalk/sasl/dh_blowfish.rb +71 -0
- data/lib/cakewalk/sasl/diffie_hellman.rb +47 -0
- data/lib/cakewalk/sasl/mechanism.rb +6 -0
- data/lib/cakewalk/sasl/plain.rb +26 -0
- data/lib/cakewalk/sasl.rb +34 -0
- data/lib/cakewalk/syncable.rb +83 -0
- data/lib/cakewalk/target.rb +199 -0
- data/lib/cakewalk/timer.rb +145 -0
- data/lib/cakewalk/user.rb +488 -0
- data/lib/cakewalk/user_list.rb +87 -0
- data/lib/cakewalk/utilities/deprecation.rb +16 -0
- data/lib/cakewalk/utilities/encoding.rb +37 -0
- data/lib/cakewalk/utilities/kernel.rb +13 -0
- data/lib/cakewalk/version.rb +4 -0
- data/lib/cakewalk.rb +5 -0
- 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,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
|