pubnub-ruby 0.0.9 → 3.3.0.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.
- data/lib/pubnub.rb +320 -235
- data/lib/pubnub_crypto.rb +53 -0
- data/lib/pubnub_deferrable.rb +37 -0
- data/lib/pubnub_request.rb +287 -0
- metadata +90 -61
- data/README +0 -55
- data/examples/history-example.rb +0 -37
- data/examples/publish-example.rb +0 -40
- data/examples/subscribe-example.rb +0 -41
- data/lib/pubnub-ruby.rb +0 -1
- data/tests/unit-test.rb +0 -75
data/lib/pubnub.rb
CHANGED
@@ -2,269 +2,354 @@
|
|
2
2
|
## http://www.pubnub.com/blog/ruby-push-api - Ruby Push API Blog
|
3
3
|
|
4
4
|
## PubNub Real Time Push APIs and Notifications Framework
|
5
|
-
## Copyright (c)
|
5
|
+
## Copyright (c) 2012 PubNub
|
6
6
|
## http://www.pubnub.com/
|
7
7
|
|
8
8
|
## -----------------------------------
|
9
|
-
## PubNub 3.
|
9
|
+
## PubNub 3.3 Real-time Push Cloud API
|
10
10
|
## -----------------------------------
|
11
11
|
|
12
|
-
|
13
|
-
##including required libraries
|
14
|
-
require 'openssl'
|
15
12
|
require 'base64'
|
16
13
|
require 'open-uri'
|
17
14
|
require 'uri'
|
18
|
-
|
19
|
-
require '
|
20
|
-
require '
|
21
|
-
require '
|
22
|
-
|
23
|
-
require '
|
24
|
-
require '
|
15
|
+
|
16
|
+
require 'pubnub_crypto'
|
17
|
+
require 'pubnub_request'
|
18
|
+
require 'pubnub_deferrable'
|
19
|
+
|
20
|
+
require 'eventmachine'
|
21
|
+
require 'uuid'
|
22
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
23
|
+
require 'active_support/core_ext/string/inflections'
|
24
|
+
require 'active_support/core_ext/object/try'
|
25
|
+
require 'active_support/core_ext/object/blank'
|
26
|
+
|
25
27
|
|
26
28
|
class Pubnub
|
27
|
-
MAX_RETRIES = 3
|
28
|
-
retries=0
|
29
|
-
|
30
|
-
#* Pubnub
|
31
|
-
#*
|
32
|
-
#* Init the Pubnub Client API
|
33
|
-
#*
|
34
|
-
#* @param string publish_key required key to send messages.
|
35
|
-
#* @param string subscribe_key required key to receive messages.
|
36
|
-
#* @param string secret_key required key to sign messages.
|
37
|
-
#*@param string cipher_key required to encrypt messages.
|
38
|
-
#* @param boolean ssl required for 2048 bit encrypted messages.
|
39
|
-
#*
|
40
|
-
def initialize( publish_key, subscribe_key, secret_key, cipher_key, ssl_on = false)
|
41
|
-
@publish_key = publish_key
|
42
|
-
@subscribe_key = subscribe_key
|
43
|
-
@secret_key = secret_key
|
44
|
-
@cipher_key = cipher_key
|
45
|
-
@ssl = ssl_on
|
46
|
-
@origin = 'pubsub.pubnub.com'
|
47
|
-
@limit = 1800
|
48
|
-
|
49
|
-
if @ssl
|
50
|
-
@origin = 'https://' + @origin
|
51
|
-
else
|
52
|
-
@origin = 'http://' + @origin
|
53
|
-
end
|
54
29
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
30
|
+
class PresenceError < RuntimeError;
|
31
|
+
end
|
32
|
+
class PublishError < RuntimeError;
|
33
|
+
end
|
34
|
+
class SubscribeError < RuntimeError;
|
35
|
+
end
|
36
|
+
class InitError < RuntimeError;
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_accessor :publish_key, :subscribe_key, :secret_key, :cipher_key, :ssl, :channel, :origin, :session_uuid
|
40
|
+
|
41
|
+
|
42
|
+
#ORIGINS = %w(newcloud-virginia.pubnub.com newcloud-california.pubnub.com newcloud-ireland.pubnub.com newcloud-tokyo.pubnub.com)
|
43
|
+
ORIGIN_HOST = 'pubsub.pubnub.com'
|
44
|
+
#ORIGIN_HOST = 'newcloud-california.pubnub.com'
|
45
|
+
#ORIGIN_HOST = 'test.pubnub.com'
|
46
|
+
|
47
|
+
def initialize(*args)
|
48
|
+
|
49
|
+
if args.size == 5 # passing in named parameters
|
50
|
+
|
51
|
+
@publish_key = args[0].to_s
|
52
|
+
@subscribe_key = args[1].to_s
|
53
|
+
@secret_key = args[2].to_s
|
54
|
+
@cipher_key = args[3].to_s
|
55
|
+
@ssl = args[4]
|
56
|
+
|
57
|
+
elsif args.size == 1 && args[0].class == Hash # passing in an options hash
|
58
|
+
|
59
|
+
options_hash = HashWithIndifferentAccess.new(args[0])
|
60
|
+
@publish_key = options_hash[:publish_key].blank? ? nil : options_hash[:publish_key].to_s
|
61
|
+
@subscribe_key = options_hash[:subscribe_key].blank? ? nil : options_hash[:subscribe_key].to_s
|
62
|
+
@secret_key = options_hash[:secret_key].blank? ? nil : options_hash[:secret_key].to_s
|
63
|
+
@cipher_key = options_hash[:cipher_key].blank? ? nil : options_hash[:cipher_key].to_s
|
64
|
+
@ssl = options_hash[:ssl].blank? ? false : true
|
65
|
+
|
66
|
+
else
|
67
|
+
raise(InitError, "Initialize with either a hash of options, or exactly 5 named parameters.")
|
62
68
|
end
|
63
69
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
70
|
+
@session_uuid = uuid
|
71
|
+
verify_init
|
72
|
+
end
|
73
|
+
|
74
|
+
def verify_init
|
75
|
+
# publish_key and cipher_key are both optional.
|
76
|
+
raise(InitError, "subscribe_key is a mandatory parameter.") if @subscribe_key.blank?
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
def publish(options)
|
81
|
+
options = HashWithIndifferentAccess.new(options)
|
82
|
+
publish_request = PubnubRequest.new(:operation => :publish)
|
83
|
+
|
84
|
+
#TODO: refactor into initializer code on request instantiation
|
85
|
+
|
86
|
+
publish_request.ssl = @ssl
|
87
|
+
publish_request.set_channel(options)
|
88
|
+
publish_request.set_callback(options)
|
89
|
+
publish_request.set_cipher_key(options, self.cipher_key)
|
90
|
+
publish_request.set_message(options, self.cipher_key)
|
91
|
+
publish_request.set_publish_key(options, self.publish_key)
|
92
|
+
publish_request.set_subscribe_key(options, self.subscribe_key)
|
93
|
+
publish_request.set_secret_key(options, self.secret_key)
|
94
|
+
|
95
|
+
|
96
|
+
publish_request.format_url!
|
97
|
+
|
98
|
+
_request(publish_request)
|
99
|
+
end
|
100
|
+
|
101
|
+
def subscribe(options)
|
102
|
+
options = HashWithIndifferentAccess.new(options)
|
103
|
+
|
104
|
+
operation = options[:operation].nil? ? :subscribe : :presence
|
105
|
+
|
106
|
+
subscribe_request = PubnubRequest.new(:operation => operation, :session_uuid => @session_uuid)
|
107
|
+
|
108
|
+
#TODO: refactor into initializer code on request instantiation
|
109
|
+
|
110
|
+
subscribe_request.ssl = @ssl
|
111
|
+
subscribe_request.set_channel(options)
|
112
|
+
subscribe_request.set_callback(options)
|
113
|
+
subscribe_request.set_cipher_key(options, self.cipher_key) unless subscribe_request.operation == "presence"
|
114
|
+
|
115
|
+
subscribe_request.set_subscribe_key(options, self.subscribe_key)
|
116
|
+
|
117
|
+
format_url_options = options[:override_timetoken].present? ? options[:override_timetoken] : nil
|
118
|
+
subscribe_request.format_url!(format_url_options)
|
119
|
+
|
120
|
+
_request(subscribe_request)
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
def presence(options)
|
125
|
+
usage_error = "presence() requires :channel and :callback options."
|
126
|
+
if options.class != Hash
|
127
|
+
raise(ArgumentError, usage_error)
|
72
128
|
end
|
73
129
|
|
74
|
-
|
75
|
-
#* Publish
|
76
|
-
#*
|
77
|
-
#* Send a message to a channel.
|
78
|
-
#*
|
79
|
-
#* @param array args with channel and message.
|
80
|
-
#* @return array success information.
|
81
|
-
|
82
|
-
def publish(args)
|
83
|
-
|
84
|
-
## Fail if bad input.
|
85
|
-
if !(args['channel'] && args['message'])
|
86
|
-
puts('Missing Channel or message')
|
87
|
-
return false
|
88
|
-
end
|
130
|
+
options = HashWithIndifferentAccess.new(options) unless (options == nil)
|
89
131
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
puts message
|
94
|
-
|
95
|
-
#encryption of message
|
96
|
-
if @cipher_key.length > 0
|
97
|
-
pubnubcrypto=PubnubCrypto.new('cipher_key')
|
98
|
-
message=message.chop.reverse.chop.reverse()
|
99
|
-
puts('message is->'+message)
|
100
|
-
message=pubnubcrypto.encrypt(message)
|
101
|
-
puts('Encrypted message->'+message)
|
102
|
-
message = message.strip
|
103
|
-
message = '"' +message+ '"'
|
104
|
-
end
|
105
|
-
|
106
|
-
## Sign message using HMAC
|
107
|
-
String signature = '0'
|
108
|
-
|
109
|
-
if @secret_key.length > 0
|
110
|
-
signature = "{@publish_key,@subscribe_key,@secret_key,channel,message}"
|
111
|
-
digest = OpenSSL::Digest.new("sha256")
|
112
|
-
key = [ @secret_key ]
|
113
|
-
hmac = OpenSSL::HMAC.hexdigest(digest, key.pack("H*"), signature)
|
114
|
-
signature = hmac
|
115
|
-
end
|
116
|
-
puts "signature >> "+signature
|
117
|
-
|
118
|
-
|
119
|
-
##If message length is greater than limit output fails
|
120
|
-
if message.length > @limit
|
121
|
-
puts('message TOO LONG (' + @limit.to_s + ' LIMIT)')
|
122
|
-
return [ 0, 'message Too Long.' ]
|
123
|
-
end
|
132
|
+
unless options[:channel] && options[:callback]
|
133
|
+
raise(ArgumentError, usage_error)
|
134
|
+
end
|
124
135
|
|
125
|
-
|
126
|
-
return self._request([
|
127
|
-
'publish',
|
128
|
-
@publish_key,
|
129
|
-
@subscribe_key,
|
130
|
-
signature,
|
131
|
-
channel,
|
132
|
-
'0',
|
133
|
-
message,])
|
134
|
-
end
|
135
|
-
|
136
|
-
|
137
|
-
#* Subscribe
|
138
|
-
#*
|
139
|
-
#* This is BLOCKING.
|
140
|
-
#* Listen for a message on a channel.
|
141
|
-
#*
|
142
|
-
#* @param array args with channel and message.
|
143
|
-
#* @return false on fail, array on success.
|
144
|
-
#*
|
145
|
-
def subscribe(args)
|
146
|
-
## Capture User Input
|
147
|
-
channel = args['channel']
|
148
|
-
callback = args['callback']
|
149
|
-
|
150
|
-
## Fail if missing channel
|
151
|
-
if !channel
|
152
|
-
puts "Missing Channel."
|
153
|
-
return false
|
154
|
-
end
|
136
|
+
subscribe(options.merge(:operation => "presence"))
|
155
137
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
loop do
|
164
|
-
begin
|
165
|
-
timetoken = args['timetoken'] ? args['timetoken'] : 0
|
166
|
-
|
167
|
-
## Wait for message
|
168
|
-
response = self._request([
|
169
|
-
'subscribe',
|
170
|
-
@subscribe_key,
|
171
|
-
channel,
|
172
|
-
'0',
|
173
|
-
timetoken.to_s])
|
174
|
-
messages = response[0]
|
175
|
-
args['timetoken'] = response[1]
|
176
|
-
|
177
|
-
## If it was a timeout
|
178
|
-
next if !messages.length
|
179
|
-
|
180
|
-
## Run user Callback and Reconnect if user permits.
|
181
|
-
##Capture the message and encrypt it
|
182
|
-
messages.each do |message|
|
183
|
-
pc = PubnubCrypto.new('cipher_key')
|
184
|
-
message = pc.decrypt(message)
|
185
|
-
if !callback.call(message)
|
186
|
-
return
|
187
|
-
end
|
188
|
-
end
|
189
|
-
rescue Timeout::Error
|
190
|
-
rescue
|
191
|
-
sleep(1)
|
192
|
-
end
|
193
|
-
end
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
def here_now(options = nil)
|
142
|
+
usage_error = "here_now() requires :channel and :callback options."
|
143
|
+
if options.class != Hash
|
144
|
+
raise(ArgumentError, usage_error)
|
194
145
|
end
|
195
|
-
|
196
|
-
|
197
|
-
#* History
|
198
|
-
#*
|
199
|
-
#* Load history from a channel.
|
200
|
-
#*
|
201
|
-
#* @param array args with 'channel' and 'limit'.
|
202
|
-
#* @return mixed false on fail, array on success.
|
203
|
-
#*
|
204
|
-
def history(args)
|
205
|
-
## Capture User Input
|
206
|
-
limit = +args['limit'] ? +args['limit'] : 15
|
207
|
-
channel = args['channel']
|
208
|
-
|
209
|
-
## Fail if bad input.
|
210
|
-
if (!channel)
|
211
|
-
puts 'Missing Channel.'
|
212
|
-
return false
|
213
|
-
end
|
214
146
|
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
147
|
+
options = HashWithIndifferentAccess.new(options) unless (options == nil)
|
148
|
+
|
149
|
+
unless options[:channel] && options[:callback]
|
150
|
+
raise(ArgumentError, usage_error)
|
151
|
+
end
|
152
|
+
|
153
|
+
here_now_request = PubnubRequest.new(:operation => :here_now)
|
154
|
+
|
155
|
+
here_now_request.ssl = @ssl
|
156
|
+
here_now_request.set_channel(options)
|
157
|
+
here_now_request.set_callback(options)
|
158
|
+
|
159
|
+
here_now_request.set_subscribe_key(options, self.subscribe_key)
|
160
|
+
|
161
|
+
here_now_request.format_url!
|
162
|
+
_request(here_now_request)
|
163
|
+
|
164
|
+
end
|
165
|
+
|
166
|
+
def detailed_history(options = nil)
|
167
|
+
usage_error = "detailed_history() requires :channel, :callback, and :count options."
|
168
|
+
if options.class != Hash
|
169
|
+
raise(ArgumentError, usage_error)
|
170
|
+
end
|
171
|
+
|
172
|
+
options = HashWithIndifferentAccess.new(options) unless (options == nil)
|
173
|
+
|
174
|
+
unless options[:count] && options[:channel] && options[:callback]
|
175
|
+
raise(ArgumentError, usage_error)
|
176
|
+
end
|
177
|
+
|
178
|
+
|
179
|
+
detailed_history_request = PubnubRequest.new(:operation => :detailed_history)
|
180
|
+
|
181
|
+
#TODO: refactor into initializer code on request instantiation
|
182
|
+
|
183
|
+
# /detailed_history/SUBSCRIBE_KEY/CHANNEL/JSONP_CALLBACK/LIMIT
|
184
|
+
|
185
|
+
detailed_history_request.ssl = @ssl
|
186
|
+
detailed_history_request.set_channel(options)
|
187
|
+
detailed_history_request.set_callback(options)
|
188
|
+
detailed_history_request.set_cipher_key(options, self.cipher_key)
|
189
|
+
|
190
|
+
detailed_history_request.set_subscribe_key(options, self.subscribe_key)
|
191
|
+
|
192
|
+
detailed_history_request.history_count = options[:count]
|
193
|
+
detailed_history_request.history_start = options[:start]
|
194
|
+
detailed_history_request.history_end = options[:end]
|
195
|
+
detailed_history_request.history_reverse = options[:reverse]
|
196
|
+
|
197
|
+
detailed_history_request.format_url!
|
198
|
+
_request(detailed_history_request)
|
199
|
+
|
200
|
+
end
|
201
|
+
|
202
|
+
def history(options = nil)
|
203
|
+
usage_error = "history() requires :channel, :callback, and :limit options."
|
204
|
+
if options.class != Hash
|
205
|
+
raise(ArgumentError, usage_error)
|
224
206
|
end
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
#* @return int timestamp.
|
231
|
-
#*
|
232
|
-
def time()
|
233
|
-
return self._request([
|
234
|
-
'time',
|
235
|
-
'0'
|
236
|
-
])[0]
|
207
|
+
|
208
|
+
options = HashWithIndifferentAccess.new(options) unless (options == nil)
|
209
|
+
|
210
|
+
unless options[:limit] && options[:channel] && options[:callback]
|
211
|
+
raise(ArgumentError, usage_error)
|
237
212
|
end
|
238
213
|
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
214
|
+
|
215
|
+
history_request = PubnubRequest.new(:operation => :history)
|
216
|
+
|
217
|
+
#TODO: refactor into initializer code on request instantiation
|
218
|
+
|
219
|
+
# /history/SUBSCRIBE_KEY/CHANNEL/JSONP_CALLBACK/LIMIT
|
220
|
+
|
221
|
+
history_request.ssl = @ssl
|
222
|
+
history_request.set_channel(options)
|
223
|
+
history_request.set_callback(options)
|
224
|
+
history_request.set_cipher_key(options, self.cipher_key)
|
225
|
+
|
226
|
+
history_request.set_subscribe_key(options, self.subscribe_key)
|
227
|
+
history_request.history_limit = options[:limit]
|
228
|
+
|
229
|
+
history_request.format_url!
|
230
|
+
_request(history_request)
|
231
|
+
|
232
|
+
end
|
233
|
+
|
234
|
+
def time(options)
|
235
|
+
options = HashWithIndifferentAccess.new(options)
|
236
|
+
raise(PubNubRuntimeError, "You must supply a callback.") if options['callback'].blank?
|
237
|
+
|
238
|
+
time_request = PubnubRequest.new(:operation => :time)
|
239
|
+
time_request.set_callback(options)
|
240
|
+
|
241
|
+
time_request.format_url!
|
242
|
+
_request(time_request)
|
243
|
+
end
|
244
|
+
|
245
|
+
def my_callback(x, quiet = false)
|
246
|
+
if quiet !=false
|
247
|
+
puts("mycallback says: #{x.to_s}")
|
248
|
+
else
|
249
|
+
""
|
255
250
|
end
|
256
251
|
|
257
|
-
|
258
|
-
|
259
|
-
|
252
|
+
end
|
253
|
+
|
254
|
+
def uuid
|
255
|
+
UUID.new.generate
|
256
|
+
end
|
257
|
+
|
258
|
+
def _request(request)
|
259
|
+
|
260
|
+
if (defined?(Rails) && Rails.present? && Rails.env.present?) && Rails.env.test?
|
261
|
+
|
262
|
+
open(request.url, 'r', :read_timeout => 300) do |response|
|
263
|
+
request.package_response!(response.read)
|
264
|
+
request.callback.call(request.response)
|
265
|
+
request.response
|
266
|
+
end
|
267
|
+
|
268
|
+
else
|
269
|
+
|
270
|
+
request.format_url!
|
271
|
+
#puts("- Fetching #{request.url}")
|
260
272
|
|
261
273
|
begin
|
262
|
-
@connection.get(url).body
|
263
|
-
rescue
|
264
274
|
|
265
|
-
|
266
|
-
|
275
|
+
EM.run do
|
276
|
+
|
277
|
+
conn = PubnubDeferrable.connect request.host, request.port
|
278
|
+
conn.pubnub_request = request
|
279
|
+
req = conn.get(request.query)
|
280
|
+
|
281
|
+
timeout_timer = EM.add_periodic_timer(290) do
|
282
|
+
#puts("#{Time.now}: Reconnecting from timeout.")
|
283
|
+
reconnect_and_query(conn, request)
|
284
|
+
end
|
285
|
+
|
286
|
+
error_timer = EM.add_periodic_timer(5) do
|
287
|
+
#puts("#{Time.now}: Checking for errors.")
|
288
|
+
if conn.error?
|
289
|
+
|
290
|
+
error_message = "Intermittent Error: #{response.status}, extended info: #{response.internal_error}"
|
291
|
+
puts(error_message)
|
292
|
+
request.callback.call([0, error_message])
|
293
|
+
|
294
|
+
reconnect_and_query(conn, request)
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
req.errback do |response|
|
299
|
+
conn.close_connection
|
300
|
+
error_message = "Unknown Error: #{response.to_s}"
|
301
|
+
|
302
|
+
puts(error_message)
|
303
|
+
request.callback.call([0, error_message])
|
304
|
+
|
305
|
+
reconnect_and_query(conn, request)
|
306
|
+
end
|
307
|
+
|
308
|
+
req.callback do |response|
|
309
|
+
|
310
|
+
if response.status != 200
|
311
|
+
error_message = "Server Error, status: #{response.status}, extended info: #{response.internal_error}"
|
312
|
+
|
313
|
+
puts(error_message)
|
314
|
+
request.callback.call([0, error_message])
|
315
|
+
|
316
|
+
conn.reconnect request.host, request.port
|
317
|
+
end
|
318
|
+
|
319
|
+
request.package_response!(response.content)
|
320
|
+
request.callback.call(request.response)
|
321
|
+
|
322
|
+
EM.next_tick do
|
323
|
+
if %w(subscribe presence).include?(request.operation)
|
324
|
+
conn.close_connection
|
325
|
+
|
326
|
+
timeout_timer.cancel
|
327
|
+
error_timer.cancel
|
328
|
+
|
329
|
+
_request(request)
|
330
|
+
else
|
331
|
+
conn.close_connection
|
332
|
+
return request.response
|
333
|
+
|
334
|
+
end
|
335
|
+
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
rescue EventMachine::ConnectionError => e
|
341
|
+
error_message = "Network Error: #{e.message}"
|
342
|
+
puts(error_message)
|
343
|
+
return [0, error_message]
|
267
344
|
end
|
268
345
|
|
269
346
|
end
|
270
|
-
end
|
347
|
+
end
|
348
|
+
|
349
|
+
def reconnect_and_query(conn, request)
|
350
|
+
conn.reconnect request.host, request.port
|
351
|
+
conn.pubnub_request = request
|
352
|
+
conn.get(request.query)
|
353
|
+
end
|
354
|
+
|
355
|
+
end
|