satori-rtm-sdk 0.0.1.rc1
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 +7 -0
- data/CHANGELOG.md +3 -0
- data/README.md +97 -0
- data/lib/satori-rtm-sdk.rb +13 -0
- data/lib/satori-rtm-sdk/client.rb +477 -0
- data/lib/satori-rtm-sdk/codec.rb +17 -0
- data/lib/satori-rtm-sdk/event_emitter.rb +39 -0
- data/lib/satori-rtm-sdk/logger.rb +77 -0
- data/lib/satori-rtm-sdk/model.rb +180 -0
- data/lib/satori-rtm-sdk/version.rb +5 -0
- data/lib/satori-rtm-sdk/websocket.rb +189 -0
- metadata +184 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e4b294a724bf1777e77a6694a414384215ea18d2
|
4
|
+
data.tar.gz: d505330ffc42ccd8d329b12b22405c1495792513
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bfe76c7b4fc24dc0e6620321f53ed5c2b968058da80276bc8f6227d6ec9bc5702e4496ae7f31a4f6ac71e111223404e379103341ebbe149bb4c9eb5140060cce
|
7
|
+
data.tar.gz: 5d54ce8c2363f343960a86ad03223fceed82e65ee5fc5d04a851d6ebd7bce83b3d9662d57c0b19a7ce1a599bf8a6e68a00e0b6350cc21fe4dd84180b3bc3c7a1
|
data/CHANGELOG.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
# Ruby SDK for Satori RTM
|
2
|
+
|
3
|
+
RTM is the realtime messaging service at the core of the [Satori](https://www.satori.com).
|
4
|
+
|
5
|
+
Ruby SDK makes it more convenient to use Satori RTM from [Ruby programming language](https://www.ruby-lang.org).
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Ruby SDK works on Ruby >= 2.0 and JRuby.
|
10
|
+
|
11
|
+
Install it with [RubyGems](https://rubygems.org/)
|
12
|
+
|
13
|
+
gem install satori-rtm-sdk
|
14
|
+
|
15
|
+
or add this to your Gemfile if you use [Bundler](http://gembundler.com/):
|
16
|
+
|
17
|
+
gem "satori-rtm-sdk"
|
18
|
+
|
19
|
+
## Documentation
|
20
|
+
|
21
|
+
* [Satori Ruby SDK API](...)
|
22
|
+
* [RTM API](https://www.satori.com/docs/using-satori/rtm-api)
|
23
|
+
|
24
|
+
## Getting started
|
25
|
+
|
26
|
+
Here's an example how to use Satori RTM SDK to write publish / subscribe logic:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
require 'satori-rtm-sdk'
|
30
|
+
|
31
|
+
endpoint = 'YOUR_ENDPOINT'
|
32
|
+
appkey = 'YOUR_APPKEY'
|
33
|
+
|
34
|
+
client = Satori::RTM::Client.new(endpoint, appkey)
|
35
|
+
|
36
|
+
client.connect
|
37
|
+
|
38
|
+
client.subscribe 'animals' do |_ctx, event|
|
39
|
+
case event.type
|
40
|
+
when :subscribed
|
41
|
+
puts "Subscribed to the channel: #{event.data[:subscription_id]}"
|
42
|
+
when :data
|
43
|
+
event.data[:messages].each { |msg| puts "Animal is received #{msg}" }
|
44
|
+
when :error
|
45
|
+
puts "Subscription error: #{event.data[:error]} -- #{event.data[:reason]}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
loop do
|
50
|
+
client.publish 'animals', who: 'zebra', where: [34.13, -118.32]
|
51
|
+
client.sock_read_repeatedly duration_in_secs: 2
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
## EventMachine
|
56
|
+
|
57
|
+
Ruby SDK for Satori RTM doesn't lock you into using threading or event loop frameworks, but it's ready to be used with any of those.
|
58
|
+
|
59
|
+
The example of using Ruby SDK with EventMachine can be found [here](...)
|
60
|
+
|
61
|
+
## Logging
|
62
|
+
|
63
|
+
You can enable dumping of all PDUs either from your code
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
Satori::RTM::Logger.use_std_logger(::Logger::DEBUG)
|
67
|
+
```
|
68
|
+
|
69
|
+
or by setting `DEBUG_SATORI_SDK` environment variable prior to running your application
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
$ DEBUG_SATORI_SDK=true ruby myapp.rb
|
73
|
+
```
|
74
|
+
|
75
|
+
## Testing Your Changes
|
76
|
+
|
77
|
+
Tests require an active RTM to be available. The tests require `credentials.json` to be populated with the RTM properties.
|
78
|
+
|
79
|
+
The `credentials.json` file must include the following key-value pairs:
|
80
|
+
|
81
|
+
```
|
82
|
+
{
|
83
|
+
"endpoint": "YOUR_ENDPOINT",
|
84
|
+
"appkey": "YOUR_APPKEY",
|
85
|
+
"auth_role_name": "YOUR_ROLE",
|
86
|
+
"auth_role_secret_key": "YOUR_SECRET",
|
87
|
+
"auth_restricted_channel": "YOUR_RESTRICTED_CHANNEL"
|
88
|
+
}
|
89
|
+
```
|
90
|
+
|
91
|
+
* `endpoint` is your customer-specific endpoint for RTM access.
|
92
|
+
* `appkey` is your application key.
|
93
|
+
* `auth_role_name` is a role name that permits to publish / subscribe to `auth_restricted_channel`. Must be not `default`.
|
94
|
+
* `auth_role_secret_key` is a secret key for `auth_role_name`.
|
95
|
+
* `auth_restricted_channel` is a channel with subscribe and publish access for `auth_role_name` role only.
|
96
|
+
|
97
|
+
After setting up `credentials.json`, just type `rspec spec` at the command line.
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'satori-rtm-sdk/codec'
|
2
|
+
require 'satori-rtm-sdk/event_emitter'
|
3
|
+
require 'satori-rtm-sdk/logger'
|
4
|
+
require 'satori-rtm-sdk/websocket'
|
5
|
+
require 'satori-rtm-sdk/model'
|
6
|
+
require 'satori-rtm-sdk/client'
|
7
|
+
require 'satori-rtm-sdk/version'
|
8
|
+
|
9
|
+
module Satori
|
10
|
+
# Satori RTM classes.
|
11
|
+
module RTM
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,477 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module Satori
|
5
|
+
module RTM
|
6
|
+
# The client that an application uses for accessing RTM.
|
7
|
+
#
|
8
|
+
# @!attribute [r] transport
|
9
|
+
# @return [WebSocket] the WebSocket connection
|
10
|
+
class Client
|
11
|
+
include EventEmitter
|
12
|
+
include Logger
|
13
|
+
|
14
|
+
attr_reader :transport
|
15
|
+
private :on, :fire
|
16
|
+
|
17
|
+
# Returns a new instance of Client.
|
18
|
+
#
|
19
|
+
# @param endpoint [String] RTM endpoint
|
20
|
+
# @param appkey [String] appkey used to access RTM
|
21
|
+
# @param opts [Hash] options to create a client with.
|
22
|
+
# @option opts [WebSocket] :transport WebSocket connection implementation to use
|
23
|
+
def initialize(endpoint, appkey, opts = {})
|
24
|
+
@waiters = {}
|
25
|
+
@subscriptions = {}
|
26
|
+
@url = URI.join(endpoint, 'v2?appkey=' + appkey).to_s
|
27
|
+
@id = 0
|
28
|
+
@encoder = JsonCodec.new
|
29
|
+
@transport = init_transport(opts)
|
30
|
+
@state = :init
|
31
|
+
end
|
32
|
+
|
33
|
+
# @!group I/O
|
34
|
+
|
35
|
+
# Connects to Satori RTM.
|
36
|
+
#
|
37
|
+
# @raise [ConnectionError] network error occurred when connecting
|
38
|
+
# @return [void]
|
39
|
+
def connect
|
40
|
+
raise ConnectionError, "Client is a single-use object. You can't connect twice." if @state != :init
|
41
|
+
logger.info "connecting to #{@url}"
|
42
|
+
@transport.connect(@url)
|
43
|
+
rescue => e
|
44
|
+
# generate websocket close event
|
45
|
+
on_websocket_close(1006, "Connect exception: #{e.message}")
|
46
|
+
raise e
|
47
|
+
end
|
48
|
+
|
49
|
+
# Defines a callback to call when the client is successfully connected to Satori RTM.
|
50
|
+
#
|
51
|
+
# @yield Calls when client is connected
|
52
|
+
# @return [void]
|
53
|
+
def onopen(&fn)
|
54
|
+
on :open, &fn
|
55
|
+
end
|
56
|
+
|
57
|
+
# Defines a callback to call when the client is disconnected or not able to connect to Satori RTM.
|
58
|
+
#
|
59
|
+
# If a client is not able to connect, a +onclose+ yields too with a reason.
|
60
|
+
#
|
61
|
+
# @yield Calls when client is disconnected or not able to connect
|
62
|
+
# @yieldparam close_event [CloseEvent] reason why client was closed
|
63
|
+
# @return [void]
|
64
|
+
def onclose(&fn)
|
65
|
+
on :close, &fn
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns +true+ if the client is connected.
|
69
|
+
#
|
70
|
+
# @return [Boolean] +true+ if connected, +false+ otherwise
|
71
|
+
def connected?
|
72
|
+
@state == :open
|
73
|
+
end
|
74
|
+
|
75
|
+
# Closes gracefully the connection to Satori RTM.
|
76
|
+
# @return [void]
|
77
|
+
def close
|
78
|
+
@transport.close
|
79
|
+
end
|
80
|
+
|
81
|
+
# Reads from an WebSocket with an optional timeout.
|
82
|
+
#
|
83
|
+
# If timeout is greater than zero, it specifies a maximum interval (in seconds)
|
84
|
+
# to wait for any incoming WebSocket frames. If timeout is zero,
|
85
|
+
# then the method returns without blocking. If the timeout is less
|
86
|
+
# than zero, the method blocks indefinitely.
|
87
|
+
#
|
88
|
+
# @param opts [Hash] additional options
|
89
|
+
# @option opts [Integer] :timeout_in_secs (-1) timeout for a read operation
|
90
|
+
#
|
91
|
+
# @raise [ConnectionError] network error occurred when reading data
|
92
|
+
# @return [:ok] read successfully reads data from WebSocket
|
93
|
+
# @return [:timeout] a blocking operation times out
|
94
|
+
def sock_read(opts = {})
|
95
|
+
timeout_in_secs = opts.fetch(:timeout_in_secs, -1)
|
96
|
+
if timeout_in_secs >= 0
|
97
|
+
@transport.read_with_timeout(timeout_in_secs)
|
98
|
+
else
|
99
|
+
@transport.read
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Reads from an WebSocket in a non-blocking mode.
|
104
|
+
#
|
105
|
+
# @raise [ConnectionError] network error occurred when reading data
|
106
|
+
# @return [:ok] read successfully reads data from WebSocket
|
107
|
+
# @return [:would_block] read buffer is empty
|
108
|
+
def sock_read_nonblock
|
109
|
+
@transport.read_nonblock
|
110
|
+
end
|
111
|
+
|
112
|
+
# Reads repeatedly from an WebSocket.
|
113
|
+
#
|
114
|
+
# This method repeatedly reads from an WebSocket during a specified
|
115
|
+
# time (in seconds). If duration time is greater then zero, then the
|
116
|
+
# method blocks for the duration time and reads repeatedly all
|
117
|
+
# incoming WebSocket frames. If duration time is less than zero, the
|
118
|
+
# method blocks indefinitely.
|
119
|
+
#
|
120
|
+
# @param opts [Hash] additional options
|
121
|
+
# @option opts [Integer] :duration_in_secs (-1) duration interval
|
122
|
+
#
|
123
|
+
# @raise [ConnectionError] network error occurred when reading data
|
124
|
+
# @return [void]
|
125
|
+
def sock_read_repeatedly(opts = {})
|
126
|
+
duration_in_secs = opts.fetch(:duration_in_secs, -1)
|
127
|
+
start = Time.now
|
128
|
+
loop do
|
129
|
+
diff = (Time.now - start)
|
130
|
+
break if (duration_in_secs >= 0) && (duration_in_secs <= diff)
|
131
|
+
@transport.read_with_timeout(duration_in_secs - diff)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Wait for all RTM replies for all pending requests.
|
136
|
+
#
|
137
|
+
# This method blocks until all RTM replies are received for all
|
138
|
+
# pending requests.
|
139
|
+
#
|
140
|
+
# If timeout is greater than zero, it specifies a maximum interval (in seconds)
|
141
|
+
# to wait for any incoming WebSocket frames. If the timeout is less
|
142
|
+
# than zero, the method blocks indefinitely.
|
143
|
+
#
|
144
|
+
# @note if user's callback for a reply sends new RTM request
|
145
|
+
# then this method waits it too.
|
146
|
+
#
|
147
|
+
# @param opts [Hash] additional options
|
148
|
+
# @option opts [Integer] :timeout_in_secs (-1) timeout for an operation
|
149
|
+
#
|
150
|
+
# @raise [ConnectionError] network error occurred when reading data
|
151
|
+
#
|
152
|
+
# @return [:ok] all replies are received
|
153
|
+
# @return [:timeout] a blocking operation times out
|
154
|
+
def wait_all_replies(opts = {})
|
155
|
+
timeout_in_secs = opts.fetch(:timeout_in_secs, -1)
|
156
|
+
start = Time.now
|
157
|
+
rc = :ok
|
158
|
+
loop do
|
159
|
+
break if @waiters.empty?
|
160
|
+
|
161
|
+
if timeout_in_secs >= 0
|
162
|
+
diff = (Time.now - start)
|
163
|
+
if timeout_in_secs <= diff
|
164
|
+
rc = :timeout
|
165
|
+
break
|
166
|
+
end
|
167
|
+
@transport.read_with_timeout(timeout_in_secs - diff)
|
168
|
+
else
|
169
|
+
@transport.read_with_timeout(1)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
rc
|
173
|
+
end
|
174
|
+
|
175
|
+
# @!endgroup
|
176
|
+
|
177
|
+
# @!group Satori RTM operations
|
178
|
+
|
179
|
+
# Publishes a message to a channel.
|
180
|
+
#
|
181
|
+
# @param channel [String] name of the channel
|
182
|
+
# @param message [Object] message to publish
|
183
|
+
# @yield Callback for an RTM reply. If the block is not given, then
|
184
|
+
# no reply will be sent to a client, regardless of the outcome
|
185
|
+
# @yieldparam reply [BaseReply] RTM reply for delete request
|
186
|
+
# @return [void]
|
187
|
+
def publish(channel, message, &fn)
|
188
|
+
publish_opts = {
|
189
|
+
channel: channel,
|
190
|
+
message: message
|
191
|
+
}
|
192
|
+
send_r('rtm/publish', publish_opts, &fn)
|
193
|
+
end
|
194
|
+
|
195
|
+
# Reads a message in a channel.
|
196
|
+
#
|
197
|
+
# RTM returns the message at the position specified in the request.
|
198
|
+
# If there is no position specified, RTM defaults to the position of
|
199
|
+
# the latest message in the channel. A +null+ message in the reply
|
200
|
+
# PDU means that there were no messages at that position.
|
201
|
+
#
|
202
|
+
# @param channel [String] name of the channel
|
203
|
+
# @param opts [Hash] additional options for +rtm/read+ request
|
204
|
+
# @yield Callback for an RTM reply. If the block is not given, then
|
205
|
+
# no reply will be sent to a client, regardless of the outcome
|
206
|
+
# @yieldparam reply [BaseReply] RTM reply for delete request
|
207
|
+
# @return [void]
|
208
|
+
def read(channel, opts = {}, &fn)
|
209
|
+
read_opts = opts.merge channel: channel
|
210
|
+
send_r('rtm/read', read_opts, &fn)
|
211
|
+
end
|
212
|
+
|
213
|
+
# Writes the value of the specified key from the key-value store.
|
214
|
+
#
|
215
|
+
# Key is represented by a channel. In current RTM implementation
|
216
|
+
# write operation is the same as publish operation.
|
217
|
+
#
|
218
|
+
# @param channel [String] name of the channel
|
219
|
+
# @param message [Object] message to write
|
220
|
+
# @yield Callback for an RTM reply. If the block is not given, then
|
221
|
+
# no reply will be sent to a client, regardless of the outcome
|
222
|
+
# @yieldparam reply [BaseReply] RTM reply for delete request
|
223
|
+
# @return [void]
|
224
|
+
def write(channel, message, &fn)
|
225
|
+
write_opts = {
|
226
|
+
channel: channel,
|
227
|
+
message: message
|
228
|
+
}
|
229
|
+
send_r('rtm/write', write_opts, &fn)
|
230
|
+
end
|
231
|
+
|
232
|
+
# Deletes the value of the specified key from the key-value store.
|
233
|
+
#
|
234
|
+
# Key is represented by a channel, and only the last message in the
|
235
|
+
# channel is relevant (represents the value). Hence, publishing a +null+
|
236
|
+
# value, serves as deletion of the the previous value (if any). Delete request
|
237
|
+
# is the same as publishing or writing a null value to the channel.
|
238
|
+
#
|
239
|
+
# @param channel [String] name of the channel
|
240
|
+
# @yield Callback for an RTM reply. If the block is not given, then
|
241
|
+
# no reply will be sent to a client, regardless of the outcome
|
242
|
+
# @yieldparam reply [BaseReply] RTM reply for delete request
|
243
|
+
# @return [void]
|
244
|
+
def delete(channel, &fn)
|
245
|
+
delete_opts = { channel: channel }
|
246
|
+
send_r('rtm/delete', delete_opts, &fn)
|
247
|
+
end
|
248
|
+
|
249
|
+
# Subscribes to a channel
|
250
|
+
#
|
251
|
+
# When you create a subscription, you can specify additional subscription options (e.g. history or view).
|
252
|
+
# Full list of subscription option you could find in RTM API specification.
|
253
|
+
#
|
254
|
+
# Satori SDK informs an user about any subscription state changes by calling block with proper event.
|
255
|
+
#
|
256
|
+
# @see SubscriptionEvent Information about subscription events
|
257
|
+
#
|
258
|
+
# @param sid [String] subscription id
|
259
|
+
# @param opts [Hash] additional options for +rtm/subscribe+ request
|
260
|
+
# @yield RTM subscription callback
|
261
|
+
# @yieldparam ctx [SubscriptionContext] current subscription context
|
262
|
+
# @yieldparam event [SubscriptionEvent] subscription event
|
263
|
+
# @return [void]
|
264
|
+
#
|
265
|
+
# @example
|
266
|
+
# client.subscribe 'animals' do |_ctx, event|
|
267
|
+
# case event.type
|
268
|
+
# when :subscribed
|
269
|
+
# puts "Subscribed to the channel: #{event.data[:subscription_id]}"
|
270
|
+
# when :data
|
271
|
+
# event.data[:messages].each { |msg| puts "Message is received #{msg}" }
|
272
|
+
# when :error
|
273
|
+
# puts "Subscription error: #{event.data[:error]} -- #{event.data[:reason]}"
|
274
|
+
# end
|
275
|
+
# end
|
276
|
+
def subscribe(sid, opts = {}, &fn)
|
277
|
+
request_opts = opts.merge subscription_id: sid
|
278
|
+
request_opts[:channel] = sid unless %i[filter view].any? { |k| opts.key?(k) }
|
279
|
+
|
280
|
+
context = SubscriptionContext.new(sid, opts, fn)
|
281
|
+
|
282
|
+
init_reply = SubscriptionEvent.new(:init, nil)
|
283
|
+
context.fn.call(context, init_reply)
|
284
|
+
|
285
|
+
send('rtm/subscribe', request_opts) do |status, data|
|
286
|
+
reply = context.handle_data(status, data)
|
287
|
+
|
288
|
+
if @subscriptions.key?(sid) && @subscriptions[sid] != context
|
289
|
+
prev_sub = @subscriptions[sid]
|
290
|
+
prev_sub.mark_as_resubscribed
|
291
|
+
end
|
292
|
+
|
293
|
+
@subscriptions[sid] = context if reply.type == :subscribed
|
294
|
+
|
295
|
+
context.fn.call(context, reply)
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
# Unsubscribes the subscription with the specific +subscription_id+
|
300
|
+
#
|
301
|
+
# @param sid [String] subscription id
|
302
|
+
# @yield Callback for an RTM reply
|
303
|
+
# @yieldparam reply [BaseReply] RTM reply for authenticate request
|
304
|
+
# @return [void]
|
305
|
+
def unsubscribe(sid)
|
306
|
+
request_opts = { subscription_id: sid }
|
307
|
+
send('rtm/unsubscribe', request_opts) do |status, data|
|
308
|
+
context = @subscriptions.delete(sid)
|
309
|
+
if context
|
310
|
+
reply = context.handle_data(status, data)
|
311
|
+
context.fn.call(context, reply)
|
312
|
+
end
|
313
|
+
# pass base reply to unsubscribe block
|
314
|
+
yield(BaseReply.new(status, data)) if block_given?
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
# Authenticates a user with specific role and secret.
|
319
|
+
#
|
320
|
+
# Authentication is based on the +HMAC+ algorithm with +MD5+ hashing routine:
|
321
|
+
# * The SDK obtains a nonce from the RTM in a handshake request
|
322
|
+
# * The SDK then sends an authorization request with its role secret
|
323
|
+
# key hashed with the received nonce
|
324
|
+
#
|
325
|
+
# If authentication is failed then reason is passed to the yield block. In
|
326
|
+
# case of success the +rtm/authenticate+ reply is passed to the yield block.
|
327
|
+
#
|
328
|
+
# Use Dev Portal to obtain the role and secret key for your application.
|
329
|
+
#
|
330
|
+
# @param role [String] role name
|
331
|
+
# @param secret [String] role secret
|
332
|
+
# @yield Callback for an RTM reply
|
333
|
+
# @yieldparam reply [BaseReply] RTM reply for authenticate request
|
334
|
+
# @return [void]
|
335
|
+
#
|
336
|
+
# @example
|
337
|
+
# client.authenticate role, role_secret do |reply|
|
338
|
+
# raise "Failed to authenticate: #{reply.data[:error]} -- #{reply.data[:reason]}" unless reply.success?
|
339
|
+
# end
|
340
|
+
# client.wait_all_replies
|
341
|
+
def authenticate(role, secret)
|
342
|
+
handshake_opts = {
|
343
|
+
method: 'role_secret',
|
344
|
+
data: { role: role }
|
345
|
+
}
|
346
|
+
send_r('auth/handshake', handshake_opts) do |reply|
|
347
|
+
if reply.success?
|
348
|
+
hash = hmac_md5(reply.data[:data][:nonce], secret)
|
349
|
+
authenticate_opts = {
|
350
|
+
method: 'role_secret',
|
351
|
+
credentials: { hash: hash }
|
352
|
+
}
|
353
|
+
send_r('auth/authenticate', authenticate_opts) do |auth_reply|
|
354
|
+
yield(auth_reply)
|
355
|
+
end
|
356
|
+
else
|
357
|
+
yield(reply)
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
# @!endgroup
|
363
|
+
|
364
|
+
private
|
365
|
+
|
366
|
+
def pdu_to_reply_adapter(fn)
|
367
|
+
return if fn.nil?
|
368
|
+
|
369
|
+
proc do |status, data|
|
370
|
+
reply = BaseReply.new(status, data)
|
371
|
+
fn.call(reply)
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
def send_r(action, body, &fn)
|
376
|
+
send(action, body, &pdu_to_reply_adapter(fn))
|
377
|
+
end
|
378
|
+
|
379
|
+
def send(action, body, &block)
|
380
|
+
pdu = { action: action, body: body }
|
381
|
+
if block_given?
|
382
|
+
pdu[:id] = gen_next_id
|
383
|
+
@waiters[pdu[:id]] = block
|
384
|
+
end
|
385
|
+
logger.debug("-> #{pdu}")
|
386
|
+
data = @encoder.encode(pdu)
|
387
|
+
@transport.send(data, type: :text)
|
388
|
+
end
|
389
|
+
|
390
|
+
def gen_next_id
|
391
|
+
@id += 1
|
392
|
+
end
|
393
|
+
|
394
|
+
def on_websocket_open
|
395
|
+
logger.info('connection is opened')
|
396
|
+
@state = :open
|
397
|
+
fire(:open)
|
398
|
+
end
|
399
|
+
|
400
|
+
def on_websocket_close(code, reason)
|
401
|
+
return if @state == :close
|
402
|
+
@state = :close
|
403
|
+
is_normal = (code == 1000)
|
404
|
+
if is_normal
|
405
|
+
logger.info('connection is closed normally')
|
406
|
+
else
|
407
|
+
logger.warn("connection is closed with code: '#{code}' -- '#{reason}'")
|
408
|
+
end
|
409
|
+
|
410
|
+
pass_disconnect_to_all_callbacks
|
411
|
+
|
412
|
+
@waiters = {}
|
413
|
+
@subscriptions = {}
|
414
|
+
|
415
|
+
fire(:close, CloseEvent.new(code, reason))
|
416
|
+
end
|
417
|
+
|
418
|
+
def pass_disconnect_to_all_callbacks
|
419
|
+
err = { error: 'disconnect', reason: 'Connection is closed' }
|
420
|
+
|
421
|
+
@waiters.sort_by(&:first).map do |_, fn|
|
422
|
+
safe_call(fn, :disconnect, err)
|
423
|
+
end
|
424
|
+
@subscriptions.map do |_, context|
|
425
|
+
reply = context.handle_data(:disconnect, err)
|
426
|
+
safe_call(context.fn, context, reply)
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
def on_websocket_message(data, _type)
|
431
|
+
pdu = @encoder.decode(data)
|
432
|
+
logger.debug("<- #{pdu}")
|
433
|
+
id = pdu[:id]
|
434
|
+
if id.nil?
|
435
|
+
on_unsolicited_pdu(pdu)
|
436
|
+
else
|
437
|
+
fn = @waiters.delete(id)
|
438
|
+
fn.call(:pdu, pdu) unless fn.nil?
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
def on_unsolicited_pdu(pdu)
|
443
|
+
if pdu[:action] == '/error'
|
444
|
+
reason = "Unclassified RTM error is received: #{pdu[:body][:error]} -- #{pdu[:body][:reason]}"
|
445
|
+
@transport.close 1008, reason
|
446
|
+
elsif pdu[:action].start_with? 'rtm/subscription'
|
447
|
+
sid = pdu[:body][:subscription_id]
|
448
|
+
context = @subscriptions[sid]
|
449
|
+
if context
|
450
|
+
reply = context.handle_data(:pdu, pdu)
|
451
|
+
context.fn.call(context, reply)
|
452
|
+
end
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
def hmac_md5(nonce, secret)
|
457
|
+
algorithm = OpenSSL::Digest.new('md5')
|
458
|
+
digest = OpenSSL::HMAC.digest(algorithm, secret, nonce)
|
459
|
+
Base64.encode64(digest).chomp
|
460
|
+
end
|
461
|
+
|
462
|
+
def init_transport(opts)
|
463
|
+
transport = opts[:transport] || WebSocket.new
|
464
|
+
transport.on(:open, &method(:on_websocket_open))
|
465
|
+
transport.on(:message, &method(:on_websocket_message))
|
466
|
+
transport.on(:close, &method(:on_websocket_close))
|
467
|
+
transport
|
468
|
+
end
|
469
|
+
|
470
|
+
def safe_call(fn, *args)
|
471
|
+
fn.call(*args)
|
472
|
+
rescue => e
|
473
|
+
logger.error(e)
|
474
|
+
end
|
475
|
+
end
|
476
|
+
end
|
477
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Satori
|
4
|
+
module RTM
|
5
|
+
# JSON message encoder / decoder.
|
6
|
+
# @!visibility private
|
7
|
+
class JsonCodec
|
8
|
+
def encode(pdu)
|
9
|
+
JSON.generate(pdu)
|
10
|
+
end
|
11
|
+
|
12
|
+
def decode(data)
|
13
|
+
JSON.parse(data, symbolize_names: true)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Satori
|
2
|
+
module RTM
|
3
|
+
# Event emitter pattern implementation.
|
4
|
+
# @!visibility private
|
5
|
+
module EventEmitter
|
6
|
+
def self.included(klass)
|
7
|
+
klass.__send__ :include, InstanceMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
# @!visibility private
|
11
|
+
module InstanceMethods
|
12
|
+
def __handlers
|
13
|
+
@__handlers ||= {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def on(name, &fn)
|
17
|
+
get_handler(name) << fn
|
18
|
+
fn
|
19
|
+
end
|
20
|
+
|
21
|
+
def fire(name, *args)
|
22
|
+
Array.new(get_handler(name)).each do |fn|
|
23
|
+
fn.call(*args)
|
24
|
+
end
|
25
|
+
|
26
|
+
Array.new(get_handler(:*)).each do |fn|
|
27
|
+
fn.call(name, *args)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def get_handler(name)
|
34
|
+
__handlers[name] ||= []
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Satori
|
4
|
+
module RTM
|
5
|
+
# Logger for Satori RTM SDK
|
6
|
+
module Logger
|
7
|
+
ENV_FLAG = 'DEBUG_SATORI_SDK'.freeze
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def create_logger(level, output = $stderr)
|
11
|
+
logger = ::Logger.new(output)
|
12
|
+
logger.level = level
|
13
|
+
logger.progname = 'satori-rtm-sdk'
|
14
|
+
logger.formatter = lambda do |severity, datetime, progname, msg|
|
15
|
+
formatted_message = case msg
|
16
|
+
when String
|
17
|
+
msg
|
18
|
+
when Exception
|
19
|
+
format "%s (%s)\n%s",
|
20
|
+
msg.message, msg.class, (msg.backtrace || []).join("\n")
|
21
|
+
else
|
22
|
+
msg.inspect
|
23
|
+
end
|
24
|
+
format "%s [%5s] - %s: %s\n",
|
25
|
+
datetime.strftime('%H:%M:%S.%L'),
|
26
|
+
severity,
|
27
|
+
progname,
|
28
|
+
formatted_message
|
29
|
+
end
|
30
|
+
logger
|
31
|
+
end
|
32
|
+
|
33
|
+
# Sets a standard logger for all Satori RTM SDK classes.
|
34
|
+
#
|
35
|
+
# @param level [Keyword] logger log level
|
36
|
+
# @param output [IO] logger output
|
37
|
+
# @return [void]
|
38
|
+
def use_std_logger(level = default_level, output = $stderr)
|
39
|
+
use_logger create_logger(level, output)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Sets a logger for all Satori RTM SDK classes.
|
43
|
+
#
|
44
|
+
# @param value [::Logger] logger
|
45
|
+
# @return [void]
|
46
|
+
def use_logger(value)
|
47
|
+
@global_logger = value
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns current logger
|
51
|
+
#
|
52
|
+
# @return [::Logger] logger
|
53
|
+
def logger
|
54
|
+
@global_logger ||= use_std_logger
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns the default logger level
|
58
|
+
#
|
59
|
+
# @return [Keyword] default logger level
|
60
|
+
def default_level
|
61
|
+
ENV.key?(ENV_FLAG) ? ::Logger::DEBUG : ::Logger::WARN
|
62
|
+
end
|
63
|
+
|
64
|
+
def included(klass)
|
65
|
+
klass.__send__ :include, InstanceMethods
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# @!visibility private
|
70
|
+
module InstanceMethods
|
71
|
+
def logger
|
72
|
+
Satori::RTM::Logger.logger
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
module Satori
|
2
|
+
module RTM
|
3
|
+
# Event about new subscription data or subscription status change.
|
4
|
+
#
|
5
|
+
# @!attribute [r] data
|
6
|
+
# Returns an event data. In most cases it represent +body+ field from incoming
|
7
|
+
# subscribe / subscription / unsubscribe PDUs. The type of PDU is accessible by
|
8
|
+
# +type+ attribute. Information about fields in +data+ could be found in RTM API
|
9
|
+
# specification.
|
10
|
+
# @return [Hash] event data
|
11
|
+
#
|
12
|
+
# @!attribute [r] type
|
13
|
+
# Returns a type of event.
|
14
|
+
# @return [:init] event before +rtm/subscribe+ request is sent
|
15
|
+
# @return [:subscribed] event when +rtm/subscribe/ok+ is received
|
16
|
+
# @return [:unsubscribed] event when +rtm/unsubscribe/ok+ is received
|
17
|
+
# @return [:error] event when +rtm/subscription/error+ / +rtm/subscribe/error+ are received
|
18
|
+
# @return [:info] event when +rtm/subscription/info+ is received
|
19
|
+
# @return [:disconnect] event when connection is lost
|
20
|
+
class SubscriptionEvent
|
21
|
+
attr_reader :data, :type
|
22
|
+
|
23
|
+
def initialize(type, data)
|
24
|
+
if type == :pdu
|
25
|
+
@type = resolve_type(data[:action])
|
26
|
+
@data = data[:body]
|
27
|
+
else
|
28
|
+
@type = type
|
29
|
+
@data = data
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns +true+ if event is an error PDU.
|
34
|
+
#
|
35
|
+
# @return [Boolean] +true+ if event is an error PDU, +false+ otherwise
|
36
|
+
def error?
|
37
|
+
type == :error
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def resolve_type(action)
|
43
|
+
case action
|
44
|
+
when 'rtm/subscribe/ok'
|
45
|
+
:subscribed
|
46
|
+
when 'rtm/unsubscribe/ok'
|
47
|
+
:unsubscribed
|
48
|
+
when 'rtm/subscription/info'
|
49
|
+
:info
|
50
|
+
when 'rtm/subscription/data'
|
51
|
+
:data
|
52
|
+
when 'rtm/subscription/error', 'rtm/subscribe/error', 'rtm/unsubscribe/error'
|
53
|
+
:error
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Base RTM reply for a request.
|
59
|
+
#
|
60
|
+
# @!attribute [r] data
|
61
|
+
# Returns a reply data. It represent +body+ field from reply PDUs from RTM.
|
62
|
+
# Information about fields in data could be found in RTM API specification.
|
63
|
+
# @return [Hash] event data
|
64
|
+
#
|
65
|
+
# @!attribute [r] type
|
66
|
+
# Returns a type of reply.
|
67
|
+
# @return [:ok] when RTM positive reply is received
|
68
|
+
# @return [:error] when RTM error is received
|
69
|
+
# @return [:disconnect] when connection is lost
|
70
|
+
class BaseReply
|
71
|
+
attr_reader :data, :type
|
72
|
+
|
73
|
+
def initialize(type, data)
|
74
|
+
if type == :pdu
|
75
|
+
@type = data[:action].end_with?('/ok') ? :ok : :error
|
76
|
+
@data = data[:body]
|
77
|
+
else
|
78
|
+
@type = type
|
79
|
+
@data = data
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns +true+ if a reply is positive
|
84
|
+
#
|
85
|
+
# @return [Boolean] +true+ if a reply is positive, +false+ otherwise
|
86
|
+
def success?
|
87
|
+
type == :ok
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns +true+ if a reply is not positive
|
91
|
+
#
|
92
|
+
# @return [Boolean] +true+ if a reply is not positive, +false+ otherwise
|
93
|
+
def error?
|
94
|
+
!success?
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Close event with information why connection is closed or can't be established.
|
99
|
+
#
|
100
|
+
# @!attribute [r] code
|
101
|
+
# Returns a WebSocket close frame code
|
102
|
+
# @see https://tools.ietf.org/html/rfc6455 WebSocket RFC
|
103
|
+
# @return [Number] close code
|
104
|
+
#
|
105
|
+
# @!attribute [r] reason
|
106
|
+
# Returns a human-readable reason why connection is closed or can't be established.
|
107
|
+
# @return [String] close reason
|
108
|
+
class CloseEvent
|
109
|
+
attr_reader :code, :reason
|
110
|
+
|
111
|
+
def initialize(code, reason)
|
112
|
+
@code = code
|
113
|
+
@reason = reason
|
114
|
+
end
|
115
|
+
|
116
|
+
# Returns +true+ if connection was closed normally
|
117
|
+
# @return [Boolean] +true+ if connection was closed normally, +false+ otherwise
|
118
|
+
def normal?
|
119
|
+
@code == 1000
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Context with initial subscription settings and current subscription state.
|
124
|
+
#
|
125
|
+
# @!attribute [r] subscription_id
|
126
|
+
# @return [String] subscription identifier
|
127
|
+
#
|
128
|
+
# @!attribute [r] req_opts
|
129
|
+
# @return [Hash] additional options for +rtm/subscribe+ request used to create the subscription
|
130
|
+
#
|
131
|
+
# @!attribute [r] fn
|
132
|
+
# @return [Proc] RTM subscription yield block used to create the subscription
|
133
|
+
#
|
134
|
+
# @!attribute [r] position
|
135
|
+
# @return [String] current subscription position. Updated automatically after each RTM reply
|
136
|
+
#
|
137
|
+
# @!attribute [r] state
|
138
|
+
# Subscription state
|
139
|
+
# @return [:init] not established
|
140
|
+
# @return [:subscribed] subscribed
|
141
|
+
# @return [:unsubscribed] unsubscribed with +rtm/unsubscribe+ request
|
142
|
+
# @return [:disconnect] unsubscribed because connection is lost
|
143
|
+
# @return [:resubscribed] unsubscribed because new subscription replaces it with +force+ flag
|
144
|
+
class SubscriptionContext
|
145
|
+
attr_reader :subscription_id, :req_opts, :fn, :position, :state
|
146
|
+
|
147
|
+
def initialize(sid, opts, fn)
|
148
|
+
raise ArgumentError, 'subscription callback function should be specified' if fn.nil?
|
149
|
+
|
150
|
+
@subscription_id = sid
|
151
|
+
@fn = fn
|
152
|
+
@req_opts = opts
|
153
|
+
@position = nil
|
154
|
+
@state = :init
|
155
|
+
end
|
156
|
+
|
157
|
+
# @!visibility private
|
158
|
+
def handle_data(status, data)
|
159
|
+
data[:subscription_id] = @subscription_id if status == :disconnect
|
160
|
+
reply = SubscriptionEvent.new(status, data)
|
161
|
+
handle_reply(reply)
|
162
|
+
reply
|
163
|
+
end
|
164
|
+
|
165
|
+
# @!visibility private
|
166
|
+
def handle_reply(reply)
|
167
|
+
@position = reply.data[:position] if reply.data.key?(:position)
|
168
|
+
case reply.type
|
169
|
+
when :subscribed, :unsubscribed, :error, :disconnect
|
170
|
+
@state = reply.type
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# @!visibility private
|
175
|
+
def mark_as_resubscribed
|
176
|
+
@state = :resubscribed
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'openssl'
|
3
|
+
require 'websocket'
|
4
|
+
|
5
|
+
module Satori
|
6
|
+
# Satori RTM classes.
|
7
|
+
module RTM
|
8
|
+
# Error class for any connection related error.
|
9
|
+
class ConnectionError < StandardError
|
10
|
+
end
|
11
|
+
|
12
|
+
# WebSocket implementation on top of standard TCP sockets.
|
13
|
+
class WebSocket
|
14
|
+
include EventEmitter
|
15
|
+
include Logger
|
16
|
+
|
17
|
+
attr_reader :socket
|
18
|
+
|
19
|
+
def initialize(opts = {})
|
20
|
+
@opts = opts
|
21
|
+
@state = :none
|
22
|
+
end
|
23
|
+
|
24
|
+
def connect(url)
|
25
|
+
uri = URI.parse(url)
|
26
|
+
|
27
|
+
@socket = create_socket(uri)
|
28
|
+
@socket = start_tls(@socket, uri.host) if uri.scheme == 'wss'
|
29
|
+
|
30
|
+
@hs = ::WebSocket::Handshake::Client.new(url: url)
|
31
|
+
@frame = incoming_frame.new(version: @hs.version)
|
32
|
+
|
33
|
+
@state = :open
|
34
|
+
do_ws_handshake
|
35
|
+
fire :open
|
36
|
+
rescue => ex
|
37
|
+
close 1006, ex.message
|
38
|
+
raise ConnectionError, ex.message, ex.backtrace
|
39
|
+
end
|
40
|
+
|
41
|
+
def read_nonblock
|
42
|
+
data = nil
|
43
|
+
begin
|
44
|
+
data = @socket.read_nonblock(1024)
|
45
|
+
rescue IO::WaitReadable, IO::WaitWritable
|
46
|
+
return :would_block
|
47
|
+
end
|
48
|
+
|
49
|
+
@frame << data
|
50
|
+
while (frame = @frame.next)
|
51
|
+
handle_incoming_frame(frame)
|
52
|
+
end
|
53
|
+
:ok
|
54
|
+
rescue => ex
|
55
|
+
close 1006, 'Socket is closed'
|
56
|
+
raise ConnectionError, ex.message, ex.backtrace
|
57
|
+
end
|
58
|
+
|
59
|
+
def read
|
60
|
+
while (rc = read_nonblock) == :would_block
|
61
|
+
IO.select([@socket])
|
62
|
+
end
|
63
|
+
rc
|
64
|
+
rescue => ex
|
65
|
+
close 1006, 'Socket is closed'
|
66
|
+
raise ConnectionError, ex.message, ex.backtrace
|
67
|
+
end
|
68
|
+
|
69
|
+
def read_with_timeout(timeout_in_secs)
|
70
|
+
now = Time.now
|
71
|
+
while (rc = read_nonblock) == :would_block
|
72
|
+
diff = Time.now - now
|
73
|
+
if timeout_in_secs <= diff
|
74
|
+
rc = :timeout
|
75
|
+
break
|
76
|
+
end
|
77
|
+
IO.select([@socket], nil, nil, timeout_in_secs - diff)
|
78
|
+
end
|
79
|
+
rc
|
80
|
+
rescue => ex
|
81
|
+
close 1006, 'Socket is closed'
|
82
|
+
raise ConnectionError, ex.message, ex.backtrace
|
83
|
+
end
|
84
|
+
|
85
|
+
def send(data, args)
|
86
|
+
type = args[:type] || :text
|
87
|
+
send_frame_unsafe(data, type, args[:code])
|
88
|
+
rescue => ex
|
89
|
+
close 1006, 'Socket is closed'
|
90
|
+
raise ConnectionError, ex.message, ex.backtrace
|
91
|
+
end
|
92
|
+
|
93
|
+
def close(code = 1000, reason = nil)
|
94
|
+
if @state == :open
|
95
|
+
send_frame_unsafe(reason, :close, code)
|
96
|
+
elsif @state == :server_close_frame_received
|
97
|
+
# server send close frame, replying back with default code
|
98
|
+
send_frame_unsafe(reason, :close)
|
99
|
+
end
|
100
|
+
rescue => ex
|
101
|
+
# ignore
|
102
|
+
logger.info("fail to close socket: #{ex.message}")
|
103
|
+
ensure
|
104
|
+
should_trigger_on_close = opened?
|
105
|
+
@state = :closed
|
106
|
+
@socket.close if @socket
|
107
|
+
fire :close, code, reason if should_trigger_on_close
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def opened?
|
113
|
+
%i[open server_close_frame_received].include?(@state)
|
114
|
+
end
|
115
|
+
|
116
|
+
def incoming_frame
|
117
|
+
::WebSocket::Frame::Incoming::Client
|
118
|
+
end
|
119
|
+
|
120
|
+
def outgoing_frame
|
121
|
+
::WebSocket::Frame::Outgoing::Client
|
122
|
+
end
|
123
|
+
|
124
|
+
def send_frame_unsafe(data, type = :text, code = nil)
|
125
|
+
frame = create_out_frame(data, type, code)
|
126
|
+
return if frame.nil?
|
127
|
+
@socket.write frame
|
128
|
+
@socket.flush
|
129
|
+
end
|
130
|
+
|
131
|
+
def handle_incoming_frame(frame)
|
132
|
+
case frame.type
|
133
|
+
when :close
|
134
|
+
@state = :server_close_frame_received
|
135
|
+
close frame.code, frame.data
|
136
|
+
when :pong
|
137
|
+
fire :pong, frame.data
|
138
|
+
when :text
|
139
|
+
fire :message, frame.data, :text
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def create_socket(uri)
|
144
|
+
host = uri.host
|
145
|
+
port = uri.port
|
146
|
+
port ||= uri.scheme == 'wss' ? 443 : 80
|
147
|
+
TCPSocket.new(host, port)
|
148
|
+
end
|
149
|
+
|
150
|
+
def start_tls(socket, hostname)
|
151
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
152
|
+
ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
|
153
|
+
|
154
|
+
cert_store = OpenSSL::X509::Store.new
|
155
|
+
cert_store.set_default_paths
|
156
|
+
ctx.cert_store = cert_store
|
157
|
+
|
158
|
+
ssl_sock = OpenSSL::SSL::SSLSocket.new(socket, ctx)
|
159
|
+
# use undocumented method in SSLSocket to make it works with SNI
|
160
|
+
ssl_sock.hostname = hostname if ssl_sock.respond_to? :hostname=
|
161
|
+
ssl_sock.sync_close = true
|
162
|
+
ssl_sock.connect
|
163
|
+
|
164
|
+
ssl_sock
|
165
|
+
end
|
166
|
+
|
167
|
+
def do_ws_handshake
|
168
|
+
send(@hs.to_s, type: :plain)
|
169
|
+
|
170
|
+
while (line = @socket.gets)
|
171
|
+
@hs << line
|
172
|
+
break if @hs.finished?
|
173
|
+
end
|
174
|
+
|
175
|
+
unless @hs.valid?
|
176
|
+
raise ConnectionError, 'handshake error: ' + @hs.error.to_s
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def create_out_frame(data, type, code)
|
181
|
+
if type == :plain
|
182
|
+
data
|
183
|
+
else
|
184
|
+
outgoing_frame.new(version: @hs.version, data: data, type: type, code: code).to_s
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
metadata
ADDED
@@ -0,0 +1,184 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: satori-rtm-sdk
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1.rc1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andrey Vasenin
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-08-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: websocket
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: json
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: websocket-eventmachine-client
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.2'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.2'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec_junit_formatter
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.3.0
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.3.0
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.49.1
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.49.1
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: simplecov
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 0.15.0
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 0.15.0
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: yard
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 0.9.0
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 0.9.0
|
139
|
+
description:
|
140
|
+
email: sdk@satori.com
|
141
|
+
executables: []
|
142
|
+
extensions: []
|
143
|
+
extra_rdoc_files:
|
144
|
+
- README.md
|
145
|
+
files:
|
146
|
+
- CHANGELOG.md
|
147
|
+
- README.md
|
148
|
+
- lib/satori-rtm-sdk.rb
|
149
|
+
- lib/satori-rtm-sdk/client.rb
|
150
|
+
- lib/satori-rtm-sdk/codec.rb
|
151
|
+
- lib/satori-rtm-sdk/event_emitter.rb
|
152
|
+
- lib/satori-rtm-sdk/logger.rb
|
153
|
+
- lib/satori-rtm-sdk/model.rb
|
154
|
+
- lib/satori-rtm-sdk/version.rb
|
155
|
+
- lib/satori-rtm-sdk/websocket.rb
|
156
|
+
homepage: https://github.com/satori-com/satori-rtm-sdk-ruby
|
157
|
+
licenses:
|
158
|
+
- BSD-3-Clause
|
159
|
+
metadata: {}
|
160
|
+
post_install_message:
|
161
|
+
rdoc_options:
|
162
|
+
- "--main"
|
163
|
+
- README.md
|
164
|
+
- "--markup"
|
165
|
+
- markdown
|
166
|
+
require_paths:
|
167
|
+
- lib
|
168
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
169
|
+
requirements:
|
170
|
+
- - ">="
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: '0'
|
173
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
174
|
+
requirements:
|
175
|
+
- - ">"
|
176
|
+
- !ruby/object:Gem::Version
|
177
|
+
version: 1.3.1
|
178
|
+
requirements: []
|
179
|
+
rubyforge_project:
|
180
|
+
rubygems_version: 2.6.11
|
181
|
+
signing_key:
|
182
|
+
specification_version: 4
|
183
|
+
summary: Ruby SDK for Satori RTM
|
184
|
+
test_files: []
|