grinch 1.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 +180 -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/cinch.rb +5 -0
  41. data/lib/cinch/ban.rb +50 -0
  42. data/lib/cinch/bot.rb +479 -0
  43. data/lib/cinch/cached_list.rb +19 -0
  44. data/lib/cinch/callback.rb +20 -0
  45. data/lib/cinch/channel.rb +463 -0
  46. data/lib/cinch/channel_list.rb +29 -0
  47. data/lib/cinch/configuration.rb +73 -0
  48. data/lib/cinch/configuration/bot.rb +48 -0
  49. data/lib/cinch/configuration/dcc.rb +16 -0
  50. data/lib/cinch/configuration/plugins.rb +41 -0
  51. data/lib/cinch/configuration/sasl.rb +19 -0
  52. data/lib/cinch/configuration/ssl.rb +19 -0
  53. data/lib/cinch/configuration/timeouts.rb +14 -0
  54. data/lib/cinch/constants.rb +533 -0
  55. data/lib/cinch/dcc.rb +12 -0
  56. data/lib/cinch/dcc/dccable_object.rb +37 -0
  57. data/lib/cinch/dcc/incoming.rb +1 -0
  58. data/lib/cinch/dcc/incoming/send.rb +147 -0
  59. data/lib/cinch/dcc/outgoing.rb +1 -0
  60. data/lib/cinch/dcc/outgoing/send.rb +122 -0
  61. data/lib/cinch/exceptions.rb +46 -0
  62. data/lib/cinch/formatting.rb +125 -0
  63. data/lib/cinch/handler.rb +118 -0
  64. data/lib/cinch/handler_list.rb +90 -0
  65. data/lib/cinch/helpers.rb +231 -0
  66. data/lib/cinch/irc.rb +924 -0
  67. data/lib/cinch/isupport.rb +98 -0
  68. data/lib/cinch/log_filter.rb +21 -0
  69. data/lib/cinch/logger.rb +168 -0
  70. data/lib/cinch/logger/formatted_logger.rb +97 -0
  71. data/lib/cinch/logger/zcbot_logger.rb +22 -0
  72. data/lib/cinch/logger_list.rb +85 -0
  73. data/lib/cinch/mask.rb +69 -0
  74. data/lib/cinch/message.rb +392 -0
  75. data/lib/cinch/message_queue.rb +107 -0
  76. data/lib/cinch/mode_parser.rb +76 -0
  77. data/lib/cinch/network.rb +104 -0
  78. data/lib/cinch/open_ended_queue.rb +26 -0
  79. data/lib/cinch/pattern.rb +65 -0
  80. data/lib/cinch/plugin.rb +515 -0
  81. data/lib/cinch/plugin_list.rb +38 -0
  82. data/lib/cinch/rubyext/float.rb +3 -0
  83. data/lib/cinch/rubyext/module.rb +26 -0
  84. data/lib/cinch/rubyext/string.rb +33 -0
  85. data/lib/cinch/sasl.rb +34 -0
  86. data/lib/cinch/sasl/dh_blowfish.rb +71 -0
  87. data/lib/cinch/sasl/diffie_hellman.rb +47 -0
  88. data/lib/cinch/sasl/mechanism.rb +6 -0
  89. data/lib/cinch/sasl/plain.rb +26 -0
  90. data/lib/cinch/syncable.rb +83 -0
  91. data/lib/cinch/target.rb +199 -0
  92. data/lib/cinch/timer.rb +145 -0
  93. data/lib/cinch/user.rb +488 -0
  94. data/lib/cinch/user_list.rb +87 -0
  95. data/lib/cinch/utilities/deprecation.rb +16 -0
  96. data/lib/cinch/utilities/encoding.rb +37 -0
  97. data/lib/cinch/utilities/kernel.rb +13 -0
  98. data/lib/cinch/version.rb +4 -0
  99. metadata +140 -0
@@ -0,0 +1,38 @@
1
+ module Cinch
2
+ # @since 2.0.0
3
+ class PluginList < Array
4
+ def initialize(bot)
5
+ @bot = bot
6
+ super()
7
+ end
8
+
9
+ # @param [Class<Plugin>] plugin
10
+ def register_plugin(plugin)
11
+ self << plugin.new(@bot)
12
+ end
13
+
14
+ # @param [Array<Class<Plugin>>] plugins
15
+ def register_plugins(plugins)
16
+ plugins.each { |plugin| register_plugin(plugin) }
17
+ end
18
+
19
+ # @since 2.0.0
20
+ def unregister_plugin(plugin)
21
+ plugin.unregister
22
+ delete(plugin)
23
+ end
24
+
25
+ # @since 2.0.0
26
+ def unregister_plugins(plugins)
27
+ if plugins == self
28
+ plugins = self.dup
29
+ end
30
+ plugins.each { |plugin| unregister_plugin(plugin) }
31
+ end
32
+
33
+ # @since 2.0.0
34
+ def unregister_all
35
+ unregister_plugins(self)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,3 @@
1
+ unless Float.const_defined?(:INFINITY)
2
+ Float::INFINITY = 1.0/0.0
3
+ end
@@ -0,0 +1,26 @@
1
+ # Extensions to Ruby's Module class.
2
+ class Module
3
+ # Like `attr_reader`, but for defining a synchronized attribute
4
+ # reader.
5
+ #
6
+ # @api private
7
+ def synced_attr_reader(attribute)
8
+ undef_method(attribute)
9
+ define_method(attribute) do
10
+ attr(attribute)
11
+ end
12
+
13
+ define_method("#{attribute}_unsynced") do
14
+ attr(attribute, false, true)
15
+ end
16
+ end
17
+
18
+ # Like `attr_accessor`, but for defining a synchronized attribute
19
+ # accessor
20
+ #
21
+ # @api private
22
+ def synced_attr_accessor(attr)
23
+ synced_attr_reader(attr)
24
+ attr_accessor(attr)
25
+ end
26
+ end
@@ -0,0 +1,33 @@
1
+ # Extensions to Ruby's String class.
2
+ class String
3
+ # Like `String#downcase`, but respecting different IRC casemaps.
4
+ #
5
+ # @param [:rfc1459, :"strict-rfc1459", :ascii] mapping
6
+ # @return [String]
7
+ def irc_downcase(mapping)
8
+ case mapping
9
+ when :rfc1459
10
+ self.tr("A-Z[]\\\\^", "a-z{}|~")
11
+ when :"strict-rfc1459"
12
+ self.tr("A-Z[]\\\\", "a-z{}|")
13
+ else
14
+ # when :ascii or unknown/nil
15
+ self.tr("A-Z", "a-z")
16
+ end
17
+ end
18
+
19
+ # Like `String#upcase`, but respecting different IRC casemaps.
20
+ #
21
+ # @param [:rfc1459, :"strict-rfc1459", :ascii] mapping
22
+ # @return [String]
23
+ def irc_upcase(mapping)
24
+ case mapping
25
+ when :ascii
26
+ self.tr("a-z", "A-Z")
27
+ when :rfc1459
28
+ self.tr("a-z{}|~", "A-Z[]\\\\^")
29
+ when :"strict-rfc1459"
30
+ self.tr("a-z{}|", "A-Z[]\\\\")
31
+ end
32
+ end
33
+ end
data/lib/cinch/sasl.rb ADDED
@@ -0,0 +1,34 @@
1
+ require "cinch/sasl/diffie_hellman"
2
+ require "cinch/sasl/plain"
3
+ require "cinch/sasl/dh_blowfish"
4
+
5
+ module Cinch
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
+ # Cinch 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
+ # Cinch only.
29
+ #
30
+ # @api private
31
+ # @since 2.0.0
32
+ module SASL
33
+ end
34
+ end
@@ -0,0 +1,71 @@
1
+ require "openssl"
2
+ require "base64"
3
+ require "cinch/sasl/mechanism"
4
+
5
+ module Cinch
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 Cinch
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 Cinch
2
+ module SASL
3
+ class Mechanism
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,26 @@
1
+ require "base64"
2
+ require "cinch/sasl/mechanism"
3
+
4
+ module Cinch
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,83 @@
1
+ module Cinch
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 Cinch
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
+ Cinch::Utilities::Deprecation.print_deprecation("2.2.0", "Target#msg", "Target#send")
57
+ send(*args)
58
+ end
59
+
60
+ # @deprecated
61
+ def privmsg(*args)
62
+ Cinch::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(Cinch::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
+ Cinch::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
+ Cinch::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(Cinch::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