tkellem 0.9.0.beta5 → 0.9.0.beta6
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/lib/backwards_file_reader.rb +47 -0
- data/lib/tkellem/bouncer_connection.rb +1 -1
- data/lib/tkellem/daemon.rb +1 -1
- data/lib/tkellem/migrations/004_add_backlog_positions.rb +11 -0
- data/lib/tkellem/models/backlog_position.rb +3 -0
- data/lib/tkellem/plugins/backlog.rb +145 -53
- data/lib/tkellem/tkellem_bot.rb +23 -19
- data/lib/tkellem/tkellem_server.rb +1 -0
- data/lib/tkellem/version.rb +1 -1
- data/resources/bot_command_descriptions.yml +13 -0
- data/tkellem.gemspec +1 -1
- metadata +9 -6
@@ -0,0 +1,47 @@
|
|
1
|
+
class BackwardsFileReader
|
2
|
+
def self.scan(stream)
|
3
|
+
scanner = new(stream)
|
4
|
+
while line = scanner.readline
|
5
|
+
break unless yield(line)
|
6
|
+
end
|
7
|
+
scanner.sync
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(stream)
|
11
|
+
@stream = stream
|
12
|
+
@stream.seek 0, IO::SEEK_END
|
13
|
+
@pos = @stream.pos
|
14
|
+
@offset = 0
|
15
|
+
|
16
|
+
@read_size = [4096, @pos].min
|
17
|
+
@line_buffer = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def readline
|
21
|
+
if @line_buffer.size > 2 || @pos == 0
|
22
|
+
line = @line_buffer.pop
|
23
|
+
if line
|
24
|
+
@offset += line.length
|
25
|
+
end
|
26
|
+
return line
|
27
|
+
end
|
28
|
+
|
29
|
+
@pos -= @read_size
|
30
|
+
@stream.seek(@pos, IO::SEEK_SET)
|
31
|
+
@offset = 0
|
32
|
+
|
33
|
+
@line_buffer[0] = "#{@stream.read(@read_size)}#{@line_buffer[0]}"
|
34
|
+
@line_buffer[0] = @line_buffer[0].scan(%r{.*\n})
|
35
|
+
@line_buffer.flatten!
|
36
|
+
|
37
|
+
readline
|
38
|
+
end
|
39
|
+
|
40
|
+
def sync
|
41
|
+
if @offset > 0
|
42
|
+
@stream.seek(-@offset, IO::SEEK_CUR)
|
43
|
+
end
|
44
|
+
@offset = 0
|
45
|
+
@stream
|
46
|
+
end
|
47
|
+
end
|
@@ -78,7 +78,7 @@ module BouncerConnection
|
|
78
78
|
end
|
79
79
|
else
|
80
80
|
if @user
|
81
|
-
TkellemBot.run_command(msg.args.join(' '), @
|
81
|
+
TkellemBot.run_command(msg.args.join(' '), @bouncer, self) do |response|
|
82
82
|
say_as_tkellem(response)
|
83
83
|
end
|
84
84
|
end
|
data/lib/tkellem/daemon.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
|
+
require 'backwards_file_reader'
|
1
2
|
require 'fileutils'
|
3
|
+
require 'pathname'
|
2
4
|
require 'time'
|
3
5
|
|
4
6
|
require 'active_support/core_ext/class/attribute_accessors'
|
5
7
|
|
6
8
|
require 'tkellem/irc_message'
|
9
|
+
require 'tkellem/tkellem_bot'
|
7
10
|
|
8
11
|
module Tkellem
|
9
12
|
|
@@ -40,44 +43,94 @@ class Backlog
|
|
40
43
|
true
|
41
44
|
end
|
42
45
|
|
46
|
+
|
47
|
+
#### IMPL
|
48
|
+
|
49
|
+
class Device < Struct.new(:network_user, :device_name, :positions)
|
50
|
+
def initialize(*a)
|
51
|
+
super
|
52
|
+
self.positions = {}
|
53
|
+
end
|
54
|
+
|
55
|
+
def update_pos(ctx_name, pos)
|
56
|
+
# TODO: it'd be a good idea to throttle these updates to once every few seconds per ctx
|
57
|
+
# right now we're kind of harsh on the sqlite db
|
58
|
+
self.position(ctx_name).first_or_create.update_attribute(:position, pos)
|
59
|
+
end
|
60
|
+
|
61
|
+
def pos(ctx_name, pos_for_new = 0)
|
62
|
+
backlog_pos = self.position(ctx_name).first_or_initialize
|
63
|
+
if backlog_pos.new_record?
|
64
|
+
backlog_pos.position = pos_for_new
|
65
|
+
backlog_pos.save
|
66
|
+
end
|
67
|
+
backlog_pos.position
|
68
|
+
end
|
69
|
+
|
70
|
+
protected
|
71
|
+
|
72
|
+
def position(ctx_name)
|
73
|
+
self.positions[ctx_name] ||=
|
74
|
+
BacklogPosition.where(:network_user_id => network_user.id,
|
75
|
+
:context_name => ctx_name,
|
76
|
+
:device_name => device_name)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
43
80
|
def initialize(bouncer)
|
44
81
|
@bouncer = bouncer
|
82
|
+
@network_user = bouncer.network_user
|
45
83
|
@devices = {}
|
46
|
-
@
|
47
|
-
@
|
48
|
-
|
49
|
-
|
84
|
+
@dir = Pathname.new(File.expand_path("~/.tkellem/logs/#{bouncer.user.username}/#{bouncer.network.name}"))
|
85
|
+
@dir.mkpath()
|
86
|
+
end
|
87
|
+
|
88
|
+
def stream_path(ctx)
|
89
|
+
@dir + "#{ctx}.log"
|
90
|
+
end
|
91
|
+
|
92
|
+
def all_existing_ctxs
|
93
|
+
@dir.entries.select { |e| e.extname == ".log" }.map { |e| e.basename(".log").to_s }
|
50
94
|
end
|
51
95
|
|
52
|
-
def
|
53
|
-
|
96
|
+
def get_stream(ctx, for_reading = false)
|
97
|
+
mode = for_reading ? 'rb' : 'ab'
|
98
|
+
stream_path(ctx).open(mode) do |stream|
|
99
|
+
if !for_reading
|
100
|
+
stream.seek(0, IO::SEEK_END)
|
101
|
+
end
|
102
|
+
yield stream
|
103
|
+
end
|
54
104
|
end
|
55
105
|
|
56
|
-
def
|
57
|
-
|
58
|
-
return @streams[ctx] if @streams[ctx]
|
59
|
-
stream = @streams[ctx] = File.open(stream_filename(ctx), 'ab')
|
60
|
-
stream.seek(0, IO::SEEK_END)
|
61
|
-
@starting_pos[ctx] = stream.pos
|
62
|
-
stream
|
106
|
+
def stream_size(ctx)
|
107
|
+
stream_path(ctx).size
|
63
108
|
end
|
64
109
|
|
65
110
|
def get_device(conn)
|
66
|
-
@devices[conn.device_name] ||=
|
111
|
+
@devices[conn.device_name] ||= Device.new(@network_user, conn.device_name)
|
67
112
|
end
|
68
113
|
|
69
114
|
def client_connected(conn)
|
70
115
|
device = get_device(conn)
|
71
|
-
|
72
|
-
|
73
|
-
|
116
|
+
behind = all_existing_ctxs.select do |ctx_name|
|
117
|
+
eof = stream_size(ctx_name)
|
118
|
+
# default to the end of file, rather than the beginning, for new devices
|
119
|
+
# that way they don't get flooded the first time they connect
|
120
|
+
device.pos(ctx_name, eof) < eof
|
121
|
+
end
|
122
|
+
if !behind.empty?
|
123
|
+
# this device has missed messages, replay all the relevant backlogs
|
124
|
+
send_connect_backlogs(conn, device, behind)
|
74
125
|
end
|
75
126
|
end
|
76
127
|
|
77
128
|
def update_pos(ctx_name, pos)
|
129
|
+
# don't just iterate @devices here, because that may contain devices that
|
130
|
+
# have since been disconnected
|
78
131
|
@bouncer.active_conns.each do |conn|
|
79
132
|
device = get_device(conn)
|
80
|
-
device
|
133
|
+
device.update_pos(ctx_name, pos)
|
81
134
|
end
|
82
135
|
end
|
83
136
|
|
@@ -97,9 +150,7 @@ class Backlog
|
|
97
150
|
# incoming pm, fake ctx to be the sender's nick
|
98
151
|
ctx = msg.prefix.split(/[!~@]/, 2).first
|
99
152
|
end
|
100
|
-
|
101
|
-
stream.puts(Time.now.strftime("%d-%m-%Y %H:%M:%S") + " < #{'* ' if msg.action?}#{msg.prefix}: #{msg.args.last}")
|
102
|
-
update_pos(ctx, stream.pos)
|
153
|
+
write_msg(ctx, Time.now.strftime("%d-%m-%Y %H:%M:%S") + " < #{'* ' if msg.action?}#{msg.prefix}: #{msg.args.last}")
|
103
154
|
end
|
104
155
|
end
|
105
156
|
|
@@ -108,46 +159,67 @@ class Backlog
|
|
108
159
|
when 'PRIVMSG'
|
109
160
|
return if msg.ctcp? && !msg.action?
|
110
161
|
ctx = msg.args.first
|
111
|
-
|
112
|
-
|
162
|
+
write_msg(ctx, Time.now.strftime("%d-%m-%Y %H:%M:%S") + " > #{'* ' if msg.action?}#{msg.args.last}")
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def write_msg(ctx, processed_msg)
|
167
|
+
get_stream(ctx) do |stream|
|
168
|
+
stream.puts(processed_msg)
|
113
169
|
update_pos(ctx, stream.pos)
|
114
170
|
end
|
115
171
|
end
|
116
172
|
|
117
|
-
def
|
118
|
-
|
119
|
-
|
120
|
-
|
173
|
+
def send_connect_backlogs(conn, device, contexts)
|
174
|
+
contexts.each do |ctx_name|
|
175
|
+
start_pos = device.pos(ctx_name)
|
176
|
+
get_stream(ctx_name, true) do |stream|
|
177
|
+
stream.seek(start_pos)
|
178
|
+
send_backlog(conn, ctx_name, stream)
|
179
|
+
device.update_pos(ctx_name, stream.pos)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def send_backlog_since(conn, start_time, contexts)
|
185
|
+
debug "scanning for backlog from #{start_time.inspect}"
|
186
|
+
contexts.each do |ctx_name|
|
187
|
+
get_stream(ctx_name, true) do |stream|
|
188
|
+
last_line_len = 0
|
189
|
+
BackwardsFileReader.scan(stream) { |line| last_line_len = line.length; Time.parse(line[0,19]) >= start_time }
|
190
|
+
stream.seek(last_line_len, IO::SEEK_CUR)
|
191
|
+
send_backlog(conn, ctx_name, stream)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
121
195
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
# do nothing, it's good to send
|
132
|
-
end
|
196
|
+
def send_backlog(conn, ctx_name, stream)
|
197
|
+
while line = stream.gets
|
198
|
+
timestamp, msg = parse_line(line, ctx_name)
|
199
|
+
next unless msg
|
200
|
+
privmsg = msg.args.first[0] != '#'[0]
|
201
|
+
if msg.prefix
|
202
|
+
# to this user
|
203
|
+
if privmsg
|
204
|
+
msg.args[0] = @bouncer.nick
|
133
205
|
else
|
134
|
-
#
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
206
|
+
# do nothing, it's good to send
|
207
|
+
end
|
208
|
+
else
|
209
|
+
# from this user
|
210
|
+
if privmsg
|
211
|
+
# a one-on-one chat -- every client i've seen doesn't know how to
|
212
|
+
# display messages from themselves here, so we fake it by just
|
213
|
+
# adding an arrow and pretending the other user said it. shame.
|
214
|
+
msg.prefix = msg.args.first
|
215
|
+
msg.args[0] = @bouncer.nick
|
216
|
+
msg.args[-1] = "-> #{msg.args.last}"
|
217
|
+
else
|
218
|
+
# it's a room, we can just replay
|
219
|
+
msg.prefix = @bouncer.nick
|
146
220
|
end
|
147
|
-
conn.send_msg(msg.with_timestamp(timestamp))
|
148
221
|
end
|
149
|
-
|
150
|
-
device[ctx_name] = get_stream(ctx_name).pos
|
222
|
+
conn.send_msg(msg.with_timestamp(timestamp))
|
151
223
|
end
|
152
224
|
end
|
153
225
|
|
@@ -172,4 +244,24 @@ class Backlog
|
|
172
244
|
end
|
173
245
|
end
|
174
246
|
|
247
|
+
class BacklogCommand < TkellemBot::Command
|
248
|
+
register 'backlog'
|
249
|
+
|
250
|
+
def self.admin_only?
|
251
|
+
false
|
252
|
+
end
|
253
|
+
|
254
|
+
def execute
|
255
|
+
hours = args.pop.to_f
|
256
|
+
hours = 1 if hours <= 0 || hours >= (24*31)
|
257
|
+
cutoff = hours.hours.ago
|
258
|
+
backlog = Backlog.get_instance(bouncer)
|
259
|
+
rooms = [args.pop].compact
|
260
|
+
if rooms.empty?
|
261
|
+
rooms = backlog.all_existing_ctxs
|
262
|
+
end
|
263
|
+
backlog.send_backlog_since(conn, cutoff, rooms)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
175
267
|
end
|
data/lib/tkellem/tkellem_bot.rb
CHANGED
@@ -4,9 +4,9 @@ require 'yaml'
|
|
4
4
|
module Tkellem
|
5
5
|
|
6
6
|
class TkellemBot
|
7
|
-
# careful here -- if no
|
7
|
+
# careful here -- if no bouncer is given, it's assumed the command is running as
|
8
8
|
# an admin
|
9
|
-
def self.run_command(line,
|
9
|
+
def self.run_command(line, bouncer, conn, &block)
|
10
10
|
args = Shellwords.shellwords(line)
|
11
11
|
command_name = args.shift.upcase
|
12
12
|
command = commands[command_name]
|
@@ -16,11 +16,11 @@ class TkellemBot
|
|
16
16
|
return
|
17
17
|
end
|
18
18
|
|
19
|
-
command.run(args,
|
19
|
+
command.run(args, bouncer, conn, block)
|
20
20
|
end
|
21
21
|
|
22
22
|
class Command
|
23
|
-
attr_accessor :args, :
|
23
|
+
attr_accessor :args, :bouncer, :conn, :opts, :options
|
24
24
|
|
25
25
|
def self.option(name, *args)
|
26
26
|
@options ||= {}
|
@@ -62,18 +62,18 @@ class TkellemBot
|
|
62
62
|
end
|
63
63
|
end
|
64
64
|
|
65
|
-
def self.run(args_arr,
|
66
|
-
if admin_only? && !admin_user?(user)
|
65
|
+
def self.run(args_arr, bouncer, conn, block)
|
66
|
+
if admin_only? && !admin_user?(bouncer.try(:user))
|
67
67
|
block.call "You can only run #{name} as an admin."
|
68
68
|
return
|
69
69
|
end
|
70
70
|
cmd = self.new(block)
|
71
71
|
|
72
72
|
cmd.args = args_arr
|
73
|
-
cmd.
|
74
|
-
cmd.
|
73
|
+
cmd.bouncer = bouncer
|
74
|
+
cmd.conn = conn
|
75
75
|
|
76
|
-
cmd.options = build_options(user, cmd)
|
76
|
+
cmd.options = build_options(bouncer.try(:user), cmd)
|
77
77
|
cmd.options.parse!(args_arr)
|
78
78
|
|
79
79
|
cmd.execute
|
@@ -86,6 +86,10 @@ class TkellemBot
|
|
86
86
|
@opts = {}
|
87
87
|
end
|
88
88
|
|
89
|
+
def user
|
90
|
+
bouncer.try(:user)
|
91
|
+
end
|
92
|
+
|
89
93
|
def show_help
|
90
94
|
respond(options)
|
91
95
|
end
|
@@ -156,7 +160,7 @@ class TkellemBot
|
|
156
160
|
end
|
157
161
|
|
158
162
|
def modify
|
159
|
-
instance = model.
|
163
|
+
instance = model.where(find_attributes).first
|
160
164
|
new_record = false
|
161
165
|
if instance
|
162
166
|
instance.attributes = attributes
|
@@ -181,7 +185,7 @@ class TkellemBot
|
|
181
185
|
end
|
182
186
|
|
183
187
|
def remove
|
184
|
-
instance = model.
|
188
|
+
instance = model.where(find_attributes).first
|
185
189
|
if instance
|
186
190
|
instance.destroy
|
187
191
|
respond "Removed #{show(instance)}"
|
@@ -258,7 +262,7 @@ class TkellemBot
|
|
258
262
|
|
259
263
|
if opts['username']
|
260
264
|
if Command.admin_user?(user)
|
261
|
-
user = User.
|
265
|
+
user = User.where({ :username => opts['username'] }).first
|
262
266
|
else
|
263
267
|
raise Command::ArgumentError, "Only admins can change other passwords"
|
264
268
|
end
|
@@ -303,9 +307,9 @@ class TkellemBot
|
|
303
307
|
|
304
308
|
def execute
|
305
309
|
if opts['network'].present? # only settable by admins
|
306
|
-
target = Network.
|
310
|
+
target = Network.where(["name = ? AND user_id IS NULL", opts['network'].downcase]).first
|
307
311
|
else
|
308
|
-
target = network_user
|
312
|
+
target = bouncer.try(:network_user)
|
309
313
|
end
|
310
314
|
raise(Command::ArgumentError, "No network found") unless target
|
311
315
|
|
@@ -339,7 +343,7 @@ class TkellemBot
|
|
339
343
|
admin_option('public', '--public', "Create new public network. Once created, public/private status can't be modified.")
|
340
344
|
|
341
345
|
def list
|
342
|
-
public_networks = Network.
|
346
|
+
public_networks = Network.where('user_id IS NULL').to_a
|
343
347
|
user_networks = user.try(:reload).try(:networks) || []
|
344
348
|
if user_networks.present? && public_networks.present?
|
345
349
|
r "Public networks are prefixed with [P], user-specific networks with [U]."
|
@@ -362,10 +366,10 @@ class TkellemBot
|
|
362
366
|
end
|
363
367
|
|
364
368
|
if opts['network'].present?
|
365
|
-
target = Network.
|
366
|
-
target ||= Network.
|
369
|
+
target = Network.where(["name = ? AND user_id = ?", opts['network'].downcase, user.try(:id)]).first
|
370
|
+
target ||= Network.where(["name = ? AND user_id IS NULL", opts['network'].downcase]).first if self.class.admin_user?(user)
|
367
371
|
else
|
368
|
-
target =
|
372
|
+
target = bouncer.try(:network)
|
369
373
|
if target && target.public? && !self.class.admin_user?(user)
|
370
374
|
raise(Command::ArgumentError, "Only admins can modify public networks")
|
371
375
|
end
|
@@ -379,7 +383,7 @@ class TkellemBot
|
|
379
383
|
raise(Command::ArgumentError, "No network found") unless target
|
380
384
|
raise(Command::ArgumentError, "You must explicitly specify the network to remove") unless opts['network']
|
381
385
|
if uri
|
382
|
-
target.hosts.
|
386
|
+
target.hosts.where(addr_args).first.try(:destroy)
|
383
387
|
respond " #{show(target)}"
|
384
388
|
else
|
385
389
|
target.destroy
|
@@ -5,6 +5,7 @@ require 'rails/observers/activerecord/active_record'
|
|
5
5
|
require 'tkellem/bouncer_connection'
|
6
6
|
require 'tkellem/bouncer'
|
7
7
|
|
8
|
+
require 'tkellem/models/backlog_position'
|
8
9
|
require 'tkellem/models/host'
|
9
10
|
require 'tkellem/models/listen_address'
|
10
11
|
require 'tkellem/models/network'
|
data/lib/tkellem/version.rb
CHANGED
@@ -65,3 +65,16 @@ SETTING:
|
|
65
65
|
Examples:
|
66
66
|
SETTING
|
67
67
|
SETTING user_registration open
|
68
|
+
|
69
|
+
BACKLOG:
|
70
|
+
banner: "BACKLOG [<room or user>] <hours>"
|
71
|
+
help: |+
|
72
|
+
|
73
|
+
View backlog for a given room (or all rooms).
|
74
|
+
|
75
|
+
Examples:
|
76
|
+
view 3 hours of backlog for all rooms:
|
77
|
+
BACKLOG 3
|
78
|
+
view 2 hours of backlog for the #friends room:
|
79
|
+
BACKLOG #friends 2
|
80
|
+
|
data/tkellem.gemspec
CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |s|
|
|
20
20
|
s.add_dependency "eventmachine", "~> 1.0.3"
|
21
21
|
s.add_dependency "activerecord", "~> 4.0.0.rc2"
|
22
22
|
s.add_dependency "sqlite3", "~> 1.3.3"
|
23
|
-
s.add_dependency "rails-observers"
|
23
|
+
s.add_dependency "rails-observers", "~> 0.1.1"
|
24
24
|
|
25
25
|
s.add_development_dependency "rspec", "~> 2.5"
|
26
26
|
s.add_development_dependency "simplecov"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tkellem
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.9.0.
|
4
|
+
version: 0.9.0.beta6
|
5
5
|
prerelease: 6
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -64,17 +64,17 @@ dependencies:
|
|
64
64
|
requirement: !ruby/object:Gem::Requirement
|
65
65
|
none: false
|
66
66
|
requirements:
|
67
|
-
- -
|
67
|
+
- - ~>
|
68
68
|
- !ruby/object:Gem::Version
|
69
|
-
version:
|
69
|
+
version: 0.1.1
|
70
70
|
type: :runtime
|
71
71
|
prerelease: false
|
72
72
|
version_requirements: !ruby/object:Gem::Requirement
|
73
73
|
none: false
|
74
74
|
requirements:
|
75
|
-
- -
|
75
|
+
- - ~>
|
76
76
|
- !ruby/object:Gem::Version
|
77
|
-
version:
|
77
|
+
version: 0.1.1
|
78
78
|
- !ruby/object:Gem::Dependency
|
79
79
|
name: rspec
|
80
80
|
requirement: !ruby/object:Gem::Requirement
|
@@ -133,6 +133,7 @@ files:
|
|
133
133
|
- debian/source/format
|
134
134
|
- debian/tkellem.1
|
135
135
|
- examples/config.yml
|
136
|
+
- lib/backwards_file_reader.rb
|
136
137
|
- lib/tkellem.rb
|
137
138
|
- lib/tkellem/bouncer.rb
|
138
139
|
- lib/tkellem/bouncer_connection.rb
|
@@ -142,6 +143,8 @@ files:
|
|
142
143
|
- lib/tkellem/migrations/001_init_db.rb
|
143
144
|
- lib/tkellem/migrations/002_at_connect_columns.rb
|
144
145
|
- lib/tkellem/migrations/003_settings.rb
|
146
|
+
- lib/tkellem/migrations/004_add_backlog_positions.rb
|
147
|
+
- lib/tkellem/models/backlog_position.rb
|
145
148
|
- lib/tkellem/models/host.rb
|
146
149
|
- lib/tkellem/models/listen_address.rb
|
147
150
|
- lib/tkellem/models/network.rb
|
@@ -176,7 +179,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
176
179
|
version: '0'
|
177
180
|
segments:
|
178
181
|
- 0
|
179
|
-
hash:
|
182
|
+
hash: -3227479256214669237
|
180
183
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
181
184
|
none: false
|
182
185
|
requirements:
|