ewelink 1.3.0 → 2.2.1
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.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/ewelink.gemspec +1 -0
- data/lib/ewelink.rb +4 -0
- data/lib/ewelink/api.rb +272 -38
- data/lib/ewelink/runner.rb +1 -1
- metadata +22 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 957d00b0d8171519a48b1cf828cbd78c21dbde34b93f2c1930273ad460d5a770
|
4
|
+
data.tar.gz: 0c636a47ac05f8428d9e099d952a0b89a512f8e199a3a85a72624c10a76590b3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c20dfab19c133d80913716e7be2b20e47958e841e61d7b401f28b73e642358d80ce698d5fd22410ede0bb783114c0e91dfc7a1b4760ebab3738aaba3081275af
|
7
|
+
data.tar.gz: 07664117dfacf9f32fa7e0965199bed938da619b75b313207d702d5ad83ebc2ceacbb76b266e40f3e995114542a110061ad271630a3b007fc2e4f1cbb2bccc58
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
2.2.1
|
data/ewelink.gemspec
CHANGED
@@ -16,6 +16,7 @@ Gem::Specification.new do |s|
|
|
16
16
|
s.required_ruby_version = '>= 2.0.0'
|
17
17
|
|
18
18
|
s.add_dependency 'activesupport', '>= 6.0.0', '< 7.0.0'
|
19
|
+
s.add_dependency 'faye-websocket', '>= 0.11.0', '< 0.12.0'
|
19
20
|
s.add_dependency 'httparty', '>= 0.18.0', '< 0.19.0'
|
20
21
|
|
21
22
|
s.add_development_dependency 'byebug', '>= 11.0.0', '< 12.0.0'
|
data/lib/ewelink.rb
CHANGED
@@ -1,12 +1,16 @@
|
|
1
1
|
require 'active_support'
|
2
2
|
require 'active_support/core_ext'
|
3
3
|
require 'byebug' if ENV['DEBUGGER']
|
4
|
+
require 'eventmachine'
|
5
|
+
require 'faye/websocket'
|
4
6
|
require 'httparty'
|
5
7
|
require 'io/console'
|
6
8
|
require 'json'
|
7
9
|
require 'logger'
|
8
10
|
require 'openssl'
|
9
11
|
require 'optparse'
|
12
|
+
require 'set'
|
13
|
+
require 'timeout'
|
10
14
|
|
11
15
|
module Ewelink
|
12
16
|
|
data/lib/ewelink/api.rb
CHANGED
@@ -5,12 +5,15 @@ module Ewelink
|
|
5
5
|
APP_ID = 'oeVkj2lYFGnJu5XUtWisfW4utiN4u9Mq'
|
6
6
|
APP_SECRET = '6Nz4n0xA8s8qdxQf2GqurZj2Fs55FUvM'
|
7
7
|
DEFAULT_REGION = 'us'
|
8
|
+
REQUEST_TIMEOUT = 10.seconds
|
8
9
|
RF_BRIDGE_DEVICE_UIID = 28
|
9
10
|
SWITCH_DEVICES_UIIDS = [1, 5, 6, 24]
|
10
|
-
TIMEOUT = 10
|
11
11
|
URL = 'https://#{region}-api.coolkit.cc:8080'
|
12
12
|
UUID_NAMESPACE = 'e25750fb-3710-41af-b831-23224f4dd609';
|
13
13
|
VERSION = 8
|
14
|
+
WEB_SOCKET_PING_TOLERANCE_FACTOR = 1.5
|
15
|
+
SWITCH_STATUS_CHANGE_CHECK_TIMEOUT = 2.seconds
|
16
|
+
WEB_SOCKET_WAIT_INTERVAL = 0.2.seconds
|
14
17
|
|
15
18
|
attr_reader :email, :password, :phone_number
|
16
19
|
|
@@ -19,31 +22,45 @@ module Ewelink
|
|
19
22
|
@mutexs = {}
|
20
23
|
@password = password.presence || raise(Error.new(":password must be specified"))
|
21
24
|
@phone_number = phone_number.presence.try(:strip)
|
25
|
+
@web_socket_authenticated_api_keys = Set.new
|
26
|
+
@web_socket_switches_statuses = {}
|
22
27
|
raise(Error.new(":email or :phone_number must be specified")) if email.blank? && phone_number.blank?
|
23
28
|
end
|
24
29
|
|
25
30
|
def press_rf_bridge_button!(uuid)
|
26
31
|
synchronize(:press_rf_bridge_button) do
|
27
32
|
button = find_rf_bridge_button!(uuid)
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
'
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
33
|
+
web_socket_wait_for(-> { web_socket_authenticated? }) do
|
34
|
+
params = {
|
35
|
+
'action' => 'update',
|
36
|
+
'apikey' => button[:api_key],
|
37
|
+
'deviceid' => button[:device_id],
|
38
|
+
'params' => {
|
39
|
+
'cmd' => 'transmit',
|
40
|
+
'rfChl' => button[:channel],
|
41
|
+
},
|
42
|
+
'sequence' => web_socket_sequence,
|
43
|
+
'ts' => 0,
|
44
|
+
'userAgent' => 'app',
|
45
|
+
}
|
46
|
+
Ewelink.logger.debug(self.class.name) { "Pressing RF bridge button #{button[:uuid].inspect}" }
|
47
|
+
send_to_web_socket(JSON.generate(params))
|
48
|
+
true
|
49
|
+
end
|
41
50
|
end
|
42
51
|
end
|
43
52
|
|
44
53
|
def reload
|
45
|
-
Ewelink.logger.debug(self.class.name) { 'Reloading API (authentication token, devices
|
46
|
-
|
54
|
+
Ewelink.logger.debug(self.class.name) { 'Reloading API (authentication token, devices, region,...)' }
|
55
|
+
dispose_web_socket
|
56
|
+
[
|
57
|
+
:@api_keys,
|
58
|
+
:@authentication_token,
|
59
|
+
:@devices,
|
60
|
+
:@region,
|
61
|
+
:@rf_bridge_buttons,
|
62
|
+
:@switches,
|
63
|
+
].each do |variable|
|
47
64
|
remove_instance_variable(variable) if instance_variable_defined?(variable)
|
48
65
|
end
|
49
66
|
self
|
@@ -56,10 +73,12 @@ module Ewelink
|
|
56
73
|
Ewelink.logger.debug(self.class.name) { "Found #{devices.size} RF 433MHz bridge device(s)" }
|
57
74
|
end
|
58
75
|
rf_bridge_devices.each do |device|
|
76
|
+
api_key = device['apikey'].presence || next
|
59
77
|
device_id = device['deviceid'].presence || next
|
60
78
|
device_name = device['name'].presence || next
|
61
79
|
buttons = device['params']['rfList'].each do |rf|
|
62
80
|
button = {
|
81
|
+
api_key: api_key,
|
63
82
|
channel: rf['rfChl'],
|
64
83
|
device_id: device_id,
|
65
84
|
device_name: device_name,
|
@@ -83,15 +102,23 @@ module Ewelink
|
|
83
102
|
|
84
103
|
def switch_on?(uuid)
|
85
104
|
switch = find_switch!(uuid)
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
105
|
+
if @web_socket_switches_statuses[switch[:uuid]].nil?
|
106
|
+
web_socket_wait_for(-> { web_socket_authenticated? }) do
|
107
|
+
Ewelink.logger.debug(self.class.name) { "Checking switch #{switch[:uuid].inspect} status" }
|
108
|
+
params = {
|
109
|
+
'action' => 'query',
|
110
|
+
'apikey' => switch[:api_key],
|
111
|
+
'deviceid' => switch[:device_id],
|
112
|
+
'sequence' => web_socket_sequence,
|
113
|
+
'ts' => 0,
|
114
|
+
'userAgent' => 'app',
|
115
|
+
}
|
116
|
+
send_to_web_socket(JSON.generate(params))
|
117
|
+
end
|
118
|
+
end
|
119
|
+
web_socket_wait_for(-> { !@web_socket_switches_statuses[switch[:uuid]].nil? }) do
|
120
|
+
@web_socket_switches_statuses[switch[:uuid]] == 'on'
|
121
|
+
end
|
95
122
|
end
|
96
123
|
|
97
124
|
def switches
|
@@ -99,9 +126,11 @@ module Ewelink
|
|
99
126
|
@switches ||= [].tap do |switches|
|
100
127
|
switch_devices = devices.select { |device| SWITCH_DEVICES_UIIDS.include?(device['uiid']) }
|
101
128
|
switch_devices.each do |device|
|
129
|
+
api_key = device['apikey'].presence || next
|
102
130
|
device_id = device['deviceid'].presence || next
|
103
131
|
name = device['name'].presence || next
|
104
132
|
switch = {
|
133
|
+
api_key: api_key,
|
105
134
|
device_id: device_id,
|
106
135
|
name: name,
|
107
136
|
}
|
@@ -119,22 +148,53 @@ module Ewelink
|
|
119
148
|
on = false
|
120
149
|
end
|
121
150
|
switch = find_switch!(uuid)
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
'
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
151
|
+
@web_socket_switches_statuses[switch[:uuid]] = nil
|
152
|
+
web_socket_wait_for(-> { web_socket_authenticated? }) do
|
153
|
+
params = {
|
154
|
+
'action' => 'update',
|
155
|
+
'apikey' => switch[:api_key],
|
156
|
+
'deviceid' => switch[:device_id],
|
157
|
+
'params' => {
|
158
|
+
'switch' => on ? 'on' : 'off',
|
159
|
+
},
|
160
|
+
'sequence' => web_socket_sequence,
|
161
|
+
'ts' => 0,
|
162
|
+
'userAgent' => 'app',
|
163
|
+
}
|
164
|
+
Ewelink.logger.debug(self.class.name) { "Turning switch #{switch[:uuid].inspect} #{on ? 'on' : 'off'}" }
|
165
|
+
send_to_web_socket(JSON.generate(params))
|
166
|
+
end
|
167
|
+
sleep(SWITCH_STATUS_CHANGE_CHECK_TIMEOUT)
|
168
|
+
switch_on?(switch[:uuid]) # Waiting for switch status update
|
133
169
|
true
|
134
170
|
end
|
135
171
|
|
136
172
|
private
|
137
173
|
|
174
|
+
def api_keys
|
175
|
+
synchronize(:api_keys) do
|
176
|
+
@api_keys ||= Set.new(devices.map { |device| device['apikey'] })
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def authenticate_web_socket_api_keys
|
181
|
+
api_keys.each do |api_key|
|
182
|
+
params = {
|
183
|
+
'action' => 'userOnline',
|
184
|
+
'apikey' => api_key,
|
185
|
+
'appid' => APP_ID,
|
186
|
+
'at' => authentication_token,
|
187
|
+
'nonce' => nonce,
|
188
|
+
'sequence' => web_socket_sequence,
|
189
|
+
'ts' => Time.now.to_i,
|
190
|
+
'userAgent' => 'app',
|
191
|
+
'version' => VERSION,
|
192
|
+
}
|
193
|
+
Ewelink.logger.debug(self.class.name) { "Authenticating WebSocket API key: #{api_key.truncate(16).inspect}" }
|
194
|
+
send_to_web_socket(JSON.generate(params))
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
138
198
|
def authentication_headers
|
139
199
|
{ 'Authorization' => "Bearer #{authentication_token}" }
|
140
200
|
end
|
@@ -179,6 +239,40 @@ module Ewelink
|
|
179
239
|
end
|
180
240
|
end
|
181
241
|
|
242
|
+
def dispose_web_socket
|
243
|
+
Ewelink.logger.debug(self.class.name) { 'Dispose WebSocket' }
|
244
|
+
@web_socket_authenticated_api_keys.clear
|
245
|
+
@web_socket_switches_statuses.clear
|
246
|
+
|
247
|
+
[@web_socket_ping_thread, @web_socket_thread].each do |thread|
|
248
|
+
next unless thread
|
249
|
+
if Thread.current == thread
|
250
|
+
thread[:stop] = true
|
251
|
+
else
|
252
|
+
thread.kill
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
if @web_socket.present?
|
257
|
+
begin
|
258
|
+
@web_socket.close if @web_socket.open?
|
259
|
+
rescue
|
260
|
+
# Ignoring close errors
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
[
|
265
|
+
:@last_web_socket_pong_at,
|
266
|
+
:@web_socket_ping_interval,
|
267
|
+
:@web_socket_ping_thread,
|
268
|
+
:@web_socket_thread,
|
269
|
+
:@web_socket_url,
|
270
|
+
:@web_socket,
|
271
|
+
].each do |variable|
|
272
|
+
remove_instance_variable(variable) if instance_variable_defined?(variable)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
182
276
|
def find_rf_bridge_button!(uuid)
|
183
277
|
rf_bridge_buttons.find { |button| button[:uuid] == uuid } || raise(Error.new("No such RF bridge button with UUID: #{uuid.inspect}"))
|
184
278
|
end
|
@@ -200,7 +294,7 @@ module Ewelink
|
|
200
294
|
method = method.to_s.upcase
|
201
295
|
headers = (options[:headers] || {}).reverse_merge('Content-Type' => 'application/json')
|
202
296
|
Ewelink.logger.debug(self.class.name) { "#{method} #{url}" }
|
203
|
-
response = HTTParty.send(method.downcase, url, options.merge(headers: headers).reverse_merge(timeout:
|
297
|
+
response = HTTParty.send(method.downcase, url, options.merge(headers: headers).reverse_merge(timeout: REQUEST_TIMEOUT))
|
204
298
|
raise(Error.new("#{method} #{url}: #{response.code}")) unless response.success?
|
205
299
|
if response['error'] == 301 && response['region'].present?
|
206
300
|
@region = response['region']
|
@@ -209,15 +303,155 @@ module Ewelink
|
|
209
303
|
end
|
210
304
|
remove_instance_variable(:@authentication_token) if instance_variable_defined?(:@authentication_token) && [401, 403].include?(response['error'])
|
211
305
|
raise(Error.new("#{method} #{url}: #{response['error']} #{response['msg']}".strip)) if response['error'].present? && response['error'] != 0
|
212
|
-
response
|
306
|
+
response.to_h
|
213
307
|
rescue Errno::ECONNREFUSED, OpenSSL::OpenSSLError, SocketError, Timeout::Error => e
|
214
308
|
raise Error.new(e)
|
215
309
|
end
|
216
310
|
|
311
|
+
def send_to_web_socket(message)
|
312
|
+
if web_socket_outdated_ping?
|
313
|
+
Ewelink.logger.warn(self.class.name) { 'WebSocket ping is outdated' }
|
314
|
+
dispose_web_socket
|
315
|
+
end
|
316
|
+
web_socket.send(message)
|
317
|
+
rescue => e
|
318
|
+
dispose_web_socket
|
319
|
+
raise Error.new(e)
|
320
|
+
end
|
321
|
+
|
322
|
+
def start_web_socket_ping_thread(interval)
|
323
|
+
@last_web_socket_pong_at = Time.now
|
324
|
+
@web_socket_ping_interval = interval
|
325
|
+
Ewelink.logger.debug(self.class.name) { "Creating thread for WebSocket ping every #{@web_socket_ping_interval} seconds" }
|
326
|
+
@web_socket_ping_thread = Thread.new do
|
327
|
+
loop do
|
328
|
+
break if Thread.current[:stop]
|
329
|
+
sleep(@web_socket_ping_interval)
|
330
|
+
Ewelink.logger.debug(self.class.name) { 'Sending WebSocket ping' }
|
331
|
+
send_to_web_socket('ping')
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
217
336
|
def synchronize(name, &block)
|
218
337
|
(@mutexs[name] ||= Mutex.new).synchronize(&block)
|
219
338
|
end
|
220
339
|
|
340
|
+
def web_socket
|
341
|
+
synchronize(:web_socket) do
|
342
|
+
next @web_socket if @web_socket
|
343
|
+
|
344
|
+
@web_socket_thread = Thread.new do
|
345
|
+
EventMachine.run do
|
346
|
+
Ewelink.logger.debug(self.class.name) { "Opening WebSocket to #{web_socket_url.inspect}" }
|
347
|
+
|
348
|
+
@web_socket = Faye::WebSocket::Client.new('wss://as-pconnect3.coolkit.cc:8080/api/ws')
|
349
|
+
|
350
|
+
@web_socket.on(:close) do |event|
|
351
|
+
Ewelink.logger.debug(self.class.name) { 'WebSocket closed' }
|
352
|
+
dispose_web_socket
|
353
|
+
end
|
354
|
+
|
355
|
+
@web_socket.on(:open) do |event|
|
356
|
+
Ewelink.logger.debug(self.class.name) { 'WebSocket opened' }
|
357
|
+
@last_web_socket_pong_at = Time.now
|
358
|
+
authenticate_web_socket_api_keys
|
359
|
+
end
|
360
|
+
|
361
|
+
@web_socket.on(:message) do |event|
|
362
|
+
message = event.data
|
363
|
+
|
364
|
+
if message == 'pong'
|
365
|
+
Ewelink.logger.debug(self.class.name) { "Received WebSocket #{message.inspect} message" }
|
366
|
+
@last_web_socket_pong_at = Time.now
|
367
|
+
next
|
368
|
+
end
|
369
|
+
|
370
|
+
begin
|
371
|
+
json = JSON.parse(message)
|
372
|
+
rescue => e
|
373
|
+
Ewelink.logger.error(self.class.name) { 'WebSocket JSON parse error' }
|
374
|
+
next
|
375
|
+
end
|
376
|
+
|
377
|
+
if json.key?('error') && json['error'] != 0
|
378
|
+
Ewelink.logger.error(self.class.name) { "WebSocket message error: #{message.inspect}" }
|
379
|
+
next
|
380
|
+
end
|
381
|
+
|
382
|
+
if !@web_socket_ping_thread && json.key?('config') && json['config']['hb'] == 1 && json['config']['hbInterval'].present?
|
383
|
+
start_web_socket_ping_thread(json['config']['hbInterval'] + 7)
|
384
|
+
end
|
385
|
+
|
386
|
+
if json['apikey'].present? && !@web_socket_authenticated_api_keys.include?(json['apikey'])
|
387
|
+
@web_socket_authenticated_api_keys << json['apikey']
|
388
|
+
Ewelink.logger.debug(self.class.name) { "WebSocket successfully authenticated API key: #{json['apikey'].truncate(16).inspect}" }
|
389
|
+
end
|
390
|
+
|
391
|
+
if json['deviceid'].present? && json['params'].is_a?(Hash) && json['params']['switch'].present?
|
392
|
+
switch = switches.find { |switch| switch[:device_id] == json['deviceid'] }
|
393
|
+
if switch.present?
|
394
|
+
@web_socket_switches_statuses[switch[:uuid]] = json['params']['switch']
|
395
|
+
Ewelink.logger.debug(self.class.name) { "Switch #{switch[:uuid].inspect} is #{@web_socket_switches_statuses[switch[:uuid]]}" }
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
Timeout.timeout(REQUEST_TIMEOUT) do
|
403
|
+
while @web_socket.blank?
|
404
|
+
sleep(WEB_SOCKET_WAIT_INTERVAL)
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
@web_socket
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
def web_socket_authenticated?
|
413
|
+
api_keys == @web_socket_authenticated_api_keys
|
414
|
+
end
|
415
|
+
|
416
|
+
def web_socket_outdated_ping?
|
417
|
+
@last_web_socket_pong_at.present? && @web_socket_ping_interval.present? && @last_web_socket_pong_at < (@web_socket_ping_interval * WEB_SOCKET_PING_TOLERANCE_FACTOR).seconds.ago
|
418
|
+
end
|
419
|
+
|
420
|
+
def web_socket_sequence
|
421
|
+
(Time.now.to_f * 1000).round.to_s
|
422
|
+
end
|
423
|
+
|
424
|
+
def web_socket_url
|
425
|
+
synchronize(:web_socket_url) do
|
426
|
+
@web_socket_url ||= begin
|
427
|
+
params = {
|
428
|
+
'accept' => 'ws',
|
429
|
+
'appid' => APP_ID,
|
430
|
+
'nonce' => nonce,
|
431
|
+
'ts' => Time.now.to_i,
|
432
|
+
'version' => VERSION,
|
433
|
+
}
|
434
|
+
response = rest_request(:post, '/dispatch/app', body: JSON.generate(params), headers: authentication_headers)
|
435
|
+
raise('Error while getting WebSocket URL') unless response['error'] == 0
|
436
|
+
domain = response['domain'].presence || raise("Can't get WebSocket server domain")
|
437
|
+
port = response['port'].presence || raise("Can't get WebSocket server port")
|
438
|
+
"wss://#{domain}:#{port}/api/ws".tap { |url| Ewelink.logger.debug(self.class.name) { "WebSocket URL is: #{url.inspect}" } }
|
439
|
+
end
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
def web_socket_wait_for(condition, &block)
|
444
|
+
web_socket # Initializes WebSocket
|
445
|
+
Timeout.timeout(REQUEST_TIMEOUT) do
|
446
|
+
while !condition.call
|
447
|
+
sleep(WEB_SOCKET_WAIT_INTERVAL)
|
448
|
+
end
|
449
|
+
block_given? ? yield : true
|
450
|
+
rescue
|
451
|
+
dispose_web_socket
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
221
455
|
end
|
222
456
|
|
223
457
|
end
|
data/lib/ewelink/runner.rb
CHANGED
@@ -9,7 +9,7 @@ module Ewelink
|
|
9
9
|
options[:turn_switches_on_uuids].each { |uuid| api.turn_switch!(uuid, :on) }
|
10
10
|
options[:turn_switches_off_uuids].each { |uuid| api.turn_switch!(uuid, :off) }
|
11
11
|
options[:press_rf_bridge_buttons_uuids].each { |uuid| api.press_rf_bridge_button!(uuid) }
|
12
|
-
puts(JSON.pretty_generate(options[:switch_status_uuids].map { |uuid| [uuid, api.switch_on?(uuid) ? 'on' : 'off'] }.to_h))
|
12
|
+
puts(JSON.pretty_generate(options[:switch_status_uuids].map { |uuid| [uuid, api.switch_on?(uuid) ? 'on' : 'off'] }.to_h)) if options[:switch_status_uuids].present?
|
13
13
|
end
|
14
14
|
|
15
15
|
private
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ewelink
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexis Toulotte
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-09-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -30,6 +30,26 @@ dependencies:
|
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
32
|
version: 7.0.0
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: faye-websocket
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 0.11.0
|
40
|
+
- - "<"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 0.12.0
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: 0.11.0
|
50
|
+
- - "<"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 0.12.0
|
33
53
|
- !ruby/object:Gem::Dependency
|
34
54
|
name: httparty
|
35
55
|
requirement: !ruby/object:Gem::Requirement
|