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