satori-rtm-sdk 0.0.1.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|