butler 1.8.1 → 1.8.2
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.
- data/CHANGELOG.txt +212 -0
- data/{README → README.txt} +0 -0
- data/Rakefile +16 -11
- data/bin/botcontrol +35 -14
- data/data/butler/dialogs/create.rb +29 -40
- data/data/butler/dialogs/create_config.rb +1 -1
- data/data/butler/dialogs/dir.rb +13 -0
- data/data/butler/dialogs/en/create.yaml +24 -10
- data/data/butler/dialogs/en/dir.yaml +5 -0
- data/data/butler/dialogs/en/help.yaml +28 -11
- data/data/butler/dialogs/en/quickcreate.yaml +14 -0
- data/data/butler/dialogs/help.rb +16 -4
- data/data/butler/dialogs/quickcreate.rb +49 -0
- data/data/butler/plugins/core/access.rb +211 -0
- data/data/butler/plugins/core/logout.rb +11 -11
- data/data/butler/plugins/core/plugins.rb +23 -41
- data/data/butler/plugins/dev/bleakhouse.rb +46 -0
- data/data/butler/plugins/games/roll.rb +1 -1
- data/data/butler/plugins/operator/deop.rb +15 -20
- data/data/butler/plugins/operator/devoice.rb +14 -20
- data/data/butler/plugins/operator/limit.rb +56 -21
- data/data/butler/plugins/operator/op.rb +15 -20
- data/data/butler/plugins/operator/voice.rb +15 -20
- data/data/butler/plugins/service/define.rb +3 -3
- data/data/butler/plugins/service/more.rb +40 -0
- data/data/butler/plugins/util/cycle.rb +1 -1
- data/data/butler/plugins/util/load.rb +5 -5
- data/data/butler/plugins/util/pong.rb +3 -2
- data/lib/access/privilege.rb +17 -0
- data/lib/access/role.rb +33 -2
- data/lib/access/savable.rb +6 -0
- data/lib/access/yamlbase.rb +1 -2
- data/lib/butler/bot.rb +40 -7
- data/lib/butler/debuglog.rb +17 -0
- data/lib/butler/dialog.rb +1 -1
- data/lib/butler/irc/{channels.rb → channellist.rb} +2 -2
- data/lib/butler/irc/client.rb +60 -79
- data/lib/butler/irc/client/filter.rb +12 -0
- data/lib/butler/irc/client/listener.rb +55 -0
- data/lib/butler/irc/client/listenerlist.rb +69 -0
- data/lib/butler/irc/hostmask.rb +31 -16
- data/lib/butler/irc/message.rb +3 -3
- data/lib/butler/irc/parser.rb +2 -2
- data/lib/butler/irc/parser/rfc2812.rb +2 -6
- data/lib/butler/irc/socket.rb +12 -6
- data/lib/butler/irc/string.rb +4 -0
- data/lib/butler/irc/user.rb +0 -6
- data/lib/butler/irc/{users.rb → userlist.rb} +2 -2
- data/lib/butler/irc/whois.rb +6 -0
- data/lib/butler/plugin.rb +48 -14
- data/lib/butler/plugin/configproxy.rb +20 -0
- data/lib/butler/plugin/mapper.rb +308 -24
- data/lib/butler/plugin/matcher.rb +3 -1
- data/lib/butler/plugin/more.rb +65 -0
- data/lib/butler/plugin/onhandlers.rb +4 -4
- data/lib/butler/plugin/trigger.rb +4 -2
- data/lib/butler/plugins.rb +1 -1
- data/lib/butler/session.rb +11 -0
- data/lib/butler/version.rb +1 -1
- data/lib/cloptions.rb +1 -1
- data/lib/diagnostics.rb +20 -0
- data/lib/dialogline.rb +1 -1
- data/lib/durations.rb +19 -6
- data/lib/event.rb +8 -5
- data/lib/installer.rb +10 -3
- data/lib/ostructfixed.rb +11 -0
- data/lib/ruby/kernel/daemonize.rb +1 -2
- data/test/butler/plugin/mapper.rb +46 -0
- metadata +28 -11
- data/CHANGELOG +0 -44
- data/data/butler/plugins/core/privilege.rb +0 -103
@@ -6,27 +6,47 @@
|
|
6
6
|
|
7
7
|
|
8
8
|
|
9
|
+
require 'thread'
|
10
|
+
|
11
|
+
|
12
|
+
|
9
13
|
class Butler
|
10
14
|
class Plugin
|
11
15
|
class ConfigProxy
|
12
16
|
def initialize(config, base="")
|
13
17
|
@config = config
|
14
18
|
@base = base
|
19
|
+
@mutex = Mutex.new
|
20
|
+
end
|
21
|
+
|
22
|
+
# Synchronize access to this piece of config
|
23
|
+
def synchronize
|
24
|
+
@mutex.synchronize {
|
25
|
+
yield self
|
26
|
+
}
|
15
27
|
end
|
16
28
|
|
29
|
+
# assign a config key, hands off to Configuration#[]= and prepends
|
30
|
+
# Plugin#base to the key
|
17
31
|
def []=(key, value)
|
18
32
|
@config[key.empty? ? @base : "#{@base}/#{key}"] = value
|
19
33
|
end
|
20
34
|
|
35
|
+
# access a config key, hands off to Configuration#[] and prepends
|
36
|
+
# Plugin#base to the key
|
21
37
|
def [](key, *args)
|
22
38
|
@config[(key.empty? ? @base : "#{@base}/#{key}"), *args]
|
23
39
|
end
|
24
40
|
|
41
|
+
# test existence of a key, hands off to Configuration#exist? and prepends
|
42
|
+
# Plugin#base to the key
|
25
43
|
def exist?(key)
|
26
44
|
@config.exist?(key.empty? ? @base : "#{@base}/#{key}")
|
27
45
|
end
|
28
46
|
alias has_key? exist?
|
29
47
|
|
48
|
+
# delete a config key, hands off to Configuration#delete and prepends
|
49
|
+
# Plugin#base to the key
|
30
50
|
def delete(key)
|
31
51
|
@config.delete(key.empty? ? @base : "#{@base}/#{key}")
|
32
52
|
end
|
data/lib/butler/plugin/mapper.rb
CHANGED
@@ -7,13 +7,94 @@
|
|
7
7
|
|
8
8
|
|
9
9
|
require 'butler/plugin'
|
10
|
-
require '
|
10
|
+
require 'ostructfixed'
|
11
11
|
|
12
12
|
|
13
13
|
|
14
14
|
class Butler
|
15
15
|
class Plugin
|
16
|
+
# For TypeMaps, to indicate that a validation failed
|
17
|
+
class ValidationFailure < RuntimeError; end
|
18
|
+
|
19
|
+
# Map arguments of a specific type to their actual value
|
20
|
+
# The mapping may raise an exception inherited from
|
21
|
+
# Butler::Plugin::ValidationFailure to indicate that the value
|
22
|
+
# didn't validate
|
23
|
+
class TypeMap
|
24
|
+
|
25
|
+
# name of the typemap
|
26
|
+
attr_reader :name
|
27
|
+
|
28
|
+
# type matches only this regex
|
29
|
+
attr_reader :regex
|
30
|
+
|
31
|
+
# validate and convert the matched value
|
32
|
+
attr_reader :validation
|
33
|
+
|
34
|
+
# Example:
|
35
|
+
# TypeMap.new "IP", /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/ do |butler, value|
|
36
|
+
# value.split(/\./).map { |e|
|
37
|
+
# i = e.to_i
|
38
|
+
# raise ValidationFailure unless i.between?(0,255)
|
39
|
+
# i
|
40
|
+
# }
|
41
|
+
# end
|
42
|
+
def initialize(name, regex, &validation)
|
43
|
+
@name = name
|
44
|
+
@regex = regex
|
45
|
+
@validation = validation
|
46
|
+
end
|
47
|
+
|
48
|
+
# map the value, can raise a ValidationFailure
|
49
|
+
def map(butler, value)
|
50
|
+
@validation ? @validation[butler, value] : value
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
16
54
|
class Mapper
|
55
|
+
include Comparable
|
56
|
+
|
57
|
+
# priority of Mappers, don't change. Mappers should run with higher priority than
|
58
|
+
# less precise invocation mechanisms.
|
59
|
+
Priority = 10
|
60
|
+
|
61
|
+
module Pattern
|
62
|
+
# A portion that may contain color information
|
63
|
+
def self.colored(item)
|
64
|
+
"#{Color}?#{Regexp.escape(item)}#{Color}?"
|
65
|
+
end
|
66
|
+
|
67
|
+
# Multiple items of a specific regex
|
68
|
+
def self.one_or_more_of(single)
|
69
|
+
"(?:#{single}(?:(?:,\\s*|\\s+)#{single})*?)"
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.one_of(items)
|
73
|
+
"(?i:#{items.map { |item| Regexp.escape(item) }.join('|')})"
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.quoted_variants(pattern)
|
77
|
+
"(?:#{pattern}|\"#{pattern}\"|'#{pattern}')"
|
78
|
+
end
|
79
|
+
|
80
|
+
# MIRC color information
|
81
|
+
Color = '(?:[\x02\x0f\x12\x1f\x1d\x09]|\cc\d{1,2}(?:,\d{1,2})?)'.freeze
|
82
|
+
|
83
|
+
# A single argument
|
84
|
+
Argument = '(?:(?:\\\\.|[^\\\\"\'\s])+|"(?:\\\\.|[^\\\\"])*"|\'(?:\\\\.|[^\\\\\'])*\')'.freeze
|
85
|
+
|
86
|
+
# A list of arguments, comma or whitespace separated
|
87
|
+
Arguments = one_or_more_of(Argument).freeze
|
88
|
+
|
89
|
+
# An arbitrary string
|
90
|
+
String = "(?:.*?)".freeze
|
91
|
+
|
92
|
+
# In the definition string, a variable definition
|
93
|
+
Variable = /([+:*])?(\w+)(?:@([\w+-]+))?(?:\{([\w,]+)\})?|(\S+)/
|
94
|
+
end
|
95
|
+
|
96
|
+
Capture = Struct.new("Capture", :name, :scan, :typemap)
|
97
|
+
|
17
98
|
attr_reader :authorization
|
18
99
|
attr_reader :hash
|
19
100
|
attr_reader :language
|
@@ -33,32 +114,55 @@ class Butler
|
|
33
114
|
@name = "mapper:#{@language}:#{@expression}".freeze
|
34
115
|
@hash = @name.hash
|
35
116
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
117
|
+
# process expression
|
118
|
+
structure = structure(expression)
|
119
|
+
raise "No Trigger" unless String === structure.first
|
120
|
+
trigger, tmp = structure.shift.split(/\s+/, 2)
|
121
|
+
trigger.strip!
|
122
|
+
structure.unshift(tmp) if tmp and !tmp.empty?
|
123
|
+
raise "No Trigger" if (!trigger || trigger.empty? || trigger =~ /\W/)
|
124
|
+
|
125
|
+
append = "^"+Pattern.colored(trigger)
|
126
|
+
captures = []
|
127
|
+
structure.each { |item|
|
128
|
+
if Array === item
|
129
|
+
optional(item, append)
|
130
|
+
else
|
131
|
+
regexify(item, append)
|
47
132
|
end
|
48
133
|
}
|
49
|
-
|
134
|
+
append << "$"
|
135
|
+
|
136
|
+
@regexp = Regexp.new(append)
|
50
137
|
end
|
51
138
|
|
52
139
|
def invoked_by?(message)
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
140
|
+
return nil unless match = @regexp.match(message.text[message.invocation.length..-1])
|
141
|
+
params = {}
|
142
|
+
butler = @plugin.butler
|
143
|
+
@captures.zip(match.captures) { |capture, value|
|
144
|
+
if value then
|
145
|
+
if capture.scan then
|
146
|
+
params[capture.name] = value.scan(capture.scan).map { |element|
|
147
|
+
if capture.typemap then
|
148
|
+
capture.typemap.map(butler, unwrap(element))
|
149
|
+
else
|
150
|
+
unwrap(element)
|
151
|
+
end
|
152
|
+
}
|
153
|
+
else
|
154
|
+
params[capture.name] = capture.typemap ? capture.typemap.map(butler, unwrap(value)) : unwrap(value)
|
155
|
+
end
|
156
|
+
else
|
157
|
+
params[capture.name] = nil
|
158
|
+
end
|
159
|
+
}
|
160
|
+
[OpenStruct.new(params)]
|
161
|
+
rescue ValidationFailure
|
162
|
+
nil
|
163
|
+
rescue Exception => e
|
164
|
+
@plugin.exception(e)
|
165
|
+
nil
|
62
166
|
end
|
63
167
|
|
64
168
|
def call(message, params)
|
@@ -66,11 +170,11 @@ class Butler
|
|
66
170
|
end
|
67
171
|
|
68
172
|
def priority
|
69
|
-
|
173
|
+
Priority
|
70
174
|
end
|
71
175
|
|
72
176
|
def <=>(other)
|
73
|
-
|
177
|
+
other.priority <=> Priority
|
74
178
|
end
|
75
179
|
|
76
180
|
def abort_invocations?
|
@@ -80,6 +184,186 @@ class Butler
|
|
80
184
|
def eql?(other)
|
81
185
|
other.kind_of?(Mapper) && @name.eql?(other.name)
|
82
186
|
end
|
187
|
+
|
188
|
+
def inspect
|
189
|
+
"#<%s:0x%08x %s %s>" % [
|
190
|
+
self.class,
|
191
|
+
object_id << 1,
|
192
|
+
@expression.inspect,
|
193
|
+
@regexp.inspect
|
194
|
+
]
|
195
|
+
end
|
196
|
+
|
197
|
+
def to_s
|
198
|
+
"#<%s:0x%08x %s>" % [
|
199
|
+
self.class,
|
200
|
+
object_id << 1,
|
201
|
+
@expression.inspect
|
202
|
+
]
|
203
|
+
end
|
204
|
+
|
205
|
+
private
|
206
|
+
# parse the optional parts of a mapping and structure it as nested array
|
207
|
+
def structure(str)
|
208
|
+
curr = []
|
209
|
+
stack = [curr]
|
210
|
+
offset = 0
|
211
|
+
o,c = str.index("[", offset), str.index("]", offset)
|
212
|
+
|
213
|
+
while o or c
|
214
|
+
if o && c && o < c then
|
215
|
+
substr = str[offset...o].strip
|
216
|
+
curr << substr unless substr.empty?
|
217
|
+
curr << []
|
218
|
+
stack << curr.last
|
219
|
+
curr = stack.last
|
220
|
+
offset = o+1
|
221
|
+
elsif c then
|
222
|
+
substr = str[offset...c].strip
|
223
|
+
curr << substr unless substr.empty?
|
224
|
+
stack.pop
|
225
|
+
curr = stack.last
|
226
|
+
offset = c+1
|
227
|
+
else
|
228
|
+
raise "Invalid expression, Orphan ["
|
229
|
+
end
|
230
|
+
o,c = str.index("[", offset), str.index("]", offset)
|
231
|
+
end
|
232
|
+
curr << str[offset..-1].strip unless offset == str.length
|
233
|
+
curr
|
234
|
+
end # structure
|
235
|
+
|
236
|
+
# recursively create the regex for structured optional parts
|
237
|
+
def optional(item, append)
|
238
|
+
append << "(?:"
|
239
|
+
item.each { |item|
|
240
|
+
if Array === item then
|
241
|
+
optional(item, append)
|
242
|
+
else
|
243
|
+
regexify(item, append)
|
244
|
+
end
|
245
|
+
}
|
246
|
+
append << ")??"
|
247
|
+
end
|
248
|
+
|
249
|
+
# convert a part into its regex pendant, parse out captures, types and restrictions
|
250
|
+
def regexify(string, append)
|
251
|
+
string.scan(Pattern::Variable) { |type, name, map, one_of, literal|
|
252
|
+
if literal then
|
253
|
+
# non-whitespace, non-word, non-argument(s) - probably interpunctuation
|
254
|
+
append << Regexp.escape(literal)
|
255
|
+
else
|
256
|
+
case type
|
257
|
+
when nil
|
258
|
+
raise "Forgot :, * or + prefix? Invalid pattern" if map or one_of
|
259
|
+
# literal
|
260
|
+
append << '\s+'+Pattern.colored(name)
|
261
|
+
when "+"
|
262
|
+
# string
|
263
|
+
append << "\\s+(#{Pattern::String})"
|
264
|
+
@captures << Capture.new(name.to_sym, nil, nil)
|
265
|
+
when ":"
|
266
|
+
# argument
|
267
|
+
raise "Unknown type '#{map}'" if map and !(typemap = @plugin.typemap(map))
|
268
|
+
if one_of then
|
269
|
+
append << "\\s+(#{Pattern.one_of(one_of.split(/,\s*/))})"
|
270
|
+
elsif map then
|
271
|
+
append << "\\s+(#{typemap.regex})"
|
272
|
+
else
|
273
|
+
append << "\\s+(#{Pattern::Argument})"
|
274
|
+
end
|
275
|
+
@captures << Capture.new(name.to_sym, nil, @plugin.typemap(map))
|
276
|
+
when "*"
|
277
|
+
# arguments
|
278
|
+
raise "Unknown type '#{map}'" if map and !(typemap = @plugin.typemap(map))
|
279
|
+
if one_of then
|
280
|
+
scan = Pattern.one_of(one_of.split(/,\s*/))
|
281
|
+
append << "\\s+(#{Pattern.one_or_more_of(scan)})"
|
282
|
+
scan = /#{scan}/
|
283
|
+
elsif map then
|
284
|
+
append << "\\s+(#{Pattern.one_or_more_of(typemap.regex)})"
|
285
|
+
scan = typemap.regex
|
286
|
+
else
|
287
|
+
append << "\\s+(#{Pattern.one_or_more_of(Pattern::Argument)})"
|
288
|
+
scan = /#{Pattern::Argument}/
|
289
|
+
end
|
290
|
+
@captures << Capture.new(name.to_sym, /#{scan}/, typemap)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
}
|
294
|
+
end
|
295
|
+
|
296
|
+
# remove quotes if necessary and unescape
|
297
|
+
def unwrap(value)
|
298
|
+
value = case value[0,1]
|
299
|
+
when '"': value[1..-2].gsub(/\\./) { |m| ['"', "'", ' '].include?(m) ? m[1,1] : m }
|
300
|
+
when "'": value[1..-2].gsub(/\\./) { |m| ['"', "'", ' '].include?(m) ? m[1,1] : m }
|
301
|
+
else value.gsub(/\\./) { |m| ['"', "'", ' '].include?(m) ? m[1,1] : m }
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
|
307
|
+
# add a mapping type for :map:'s
|
308
|
+
# The regular expression MUST NOT contain any captures!
|
309
|
+
# For grouping use (?: ... ), which does not capture
|
310
|
+
# The provided block can convert the value and do additional testing for validity
|
311
|
+
# If the value is invalid, raise a ValidationFailure
|
312
|
+
# Example:
|
313
|
+
# map_type "IP", /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/ do |butler, value|
|
314
|
+
# value.split(/\./).map { |e|
|
315
|
+
# i = e.to_i
|
316
|
+
# raise ValidationFailure unless i.between?(0,255)
|
317
|
+
# i
|
318
|
+
# }
|
319
|
+
# end
|
320
|
+
def self.map_type(name, regex, &validation)
|
321
|
+
@mapping_type[name] = TypeMap.new(name, regex, &validation)
|
322
|
+
end
|
323
|
+
|
324
|
+
# get the TypeMap for a type
|
325
|
+
def self.typemap(name)
|
326
|
+
(@mapping_type || MappingTypes)[name]
|
327
|
+
end
|
328
|
+
|
329
|
+
# Basic mapping types, includes:
|
330
|
+
# * Integer: any valid integer. Converted to a Fix- or Bignum.
|
331
|
+
# * +Integer: any positive integer. Converted to a Fix- or Bignum.
|
332
|
+
# * -Integer: any negative integer. Converted to a Fix- or Bignum.
|
333
|
+
# * Float: any valid float. Converted to a Float.
|
334
|
+
# * +Float: any positive float. Converted to a Float.
|
335
|
+
# * -Float: any negative float. Converted to a Float.
|
336
|
+
# * Nick: a valid nickname (not necessarily online)
|
337
|
+
# * User: a currently online and for butler visible user. Converted to an IRC::User.
|
338
|
+
# * Channelname: a valid channelname (not necessarily one butler participates)
|
339
|
+
# * Channel: a channel butler is in. Converted to an IRC::Channel.
|
340
|
+
MappingTypes = {}
|
341
|
+
# Provide additional warnings
|
342
|
+
def MappingTypes.[]=(name, typemap) # :nodoc:
|
343
|
+
warn "redefining TypeMap '#{name}'" if has_key?(name)
|
344
|
+
super
|
83
345
|
end
|
346
|
+
[
|
347
|
+
["Integer", %r{[+-]?\d+}, proc { |butler, value| Integer(value) }],
|
348
|
+
["+Integer", %r{\+?\d+}, proc { |butler, value| Integer(value) }],
|
349
|
+
["-Integer", %r{\-\d+}, proc { |butler, value| Integer(value) }],
|
350
|
+
["Float", %r{[+-]?\d+(?:\.\d+)}, proc { |butler, value| Float(value) }],
|
351
|
+
["+Float", %r{\+?\d+(?:\.\d+)}, proc { |butler, value| Float(value) }],
|
352
|
+
["-Float", %r{\-?\d+(?:\.\d+)}, proc { |butler, value| Float(value) }],
|
353
|
+
["Nick", %r{[0-9A-Za-z_\-\|\\\[\]\{\}\^\`]+}],
|
354
|
+
["Channelname", %r{[&#!\+][^\x07\x0A\x0D,: ]{1,50}}],
|
355
|
+
["User", %r{[0-9A-Za-z_\-\|\\\[\]\{\}\^\`]+}, proc { |butler, value|
|
356
|
+
butler.users[value]
|
357
|
+
}],
|
358
|
+
["Channel", %r{[&#!\+][^\x07\x0A\x0D,: ]{1,50}}, proc { |butler, value|
|
359
|
+
butler.channels[value]
|
360
|
+
}],
|
361
|
+
].each { |name, regex, validation|
|
362
|
+
MappingTypes[name] = TypeMap.new(
|
363
|
+
name,
|
364
|
+
Mapper::Pattern.quoted_variants(regex),
|
365
|
+
&validation
|
366
|
+
)
|
367
|
+
}
|
84
368
|
end
|
85
369
|
end
|
@@ -9,6 +9,8 @@
|
|
9
9
|
class Butler
|
10
10
|
class Plugin
|
11
11
|
class Matcher
|
12
|
+
include Comparable
|
13
|
+
|
12
14
|
attr_reader :priority
|
13
15
|
attr_reader :hash
|
14
16
|
attr_reader :en_match
|
@@ -32,7 +34,7 @@ class Butler
|
|
32
34
|
end
|
33
35
|
|
34
36
|
def <=>(other)
|
35
|
-
|
37
|
+
other.priority <=> @priority
|
36
38
|
end
|
37
39
|
|
38
40
|
def eql?(other)
|
@@ -0,0 +1,65 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2007 by Stefan Rusterholz.
|
3
|
+
# All rights reserved.
|
4
|
+
# See LICENSE.txt for permissions.
|
5
|
+
#++
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
class Butler
|
10
|
+
class Plugin
|
11
|
+
class More
|
12
|
+
MessageLength = 300
|
13
|
+
String = {
|
14
|
+
"en" => "![c(white),(black)]more...![o]".mirc_formatted,
|
15
|
+
"de" => "![c(white),(black)]mehr...![o]".mirc_formatted,
|
16
|
+
}
|
17
|
+
|
18
|
+
attr_reader :lead
|
19
|
+
attr_reader :text
|
20
|
+
attr_reader :index
|
21
|
+
attr_reader :length
|
22
|
+
|
23
|
+
def initialize(message, lead, text)
|
24
|
+
@language = message.language
|
25
|
+
@lead = !lead || lead.empty? ? nil : lead
|
26
|
+
@text = text
|
27
|
+
@index = 0
|
28
|
+
chunks = MessageLength-(lead ? lead.length+4 : 2)-tail.length
|
29
|
+
@pieces = text.scan(/.{1,#{chunks}}(?:\b|$)/m)
|
30
|
+
@length = @pieces.length
|
31
|
+
end
|
32
|
+
|
33
|
+
def succ
|
34
|
+
raise "Reached end already" if @index+1 >= @length
|
35
|
+
@pieces[@index+=1]
|
36
|
+
end
|
37
|
+
|
38
|
+
def prev
|
39
|
+
raise "Reached start already" if @index.zero?
|
40
|
+
@pieces[@index-=1]
|
41
|
+
end
|
42
|
+
|
43
|
+
def succ?
|
44
|
+
@index+1 < @length
|
45
|
+
end
|
46
|
+
|
47
|
+
def prev?
|
48
|
+
!@index.zero?
|
49
|
+
end
|
50
|
+
|
51
|
+
def current
|
52
|
+
@pieces[@index]
|
53
|
+
end
|
54
|
+
|
55
|
+
# show adds lead and tail depending on requirement (no tail for last message, no lead if empty)
|
56
|
+
def show
|
57
|
+
"#{lead+': ' if lead}#{@pieces[@index]}#{', '+tail if succ?}"
|
58
|
+
end
|
59
|
+
|
60
|
+
def tail
|
61
|
+
String[@language] || String["en"]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|