grinch 1.0.0

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