pubnub 0.1.12 → 3.3.0.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of pubnub might be problematic. Click here for more details.

data/lib/pubnub.rb CHANGED
@@ -2,291 +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) 2010 Stephen Blum
5
+ ## Copyright (c) 2012 PubNub
6
6
  ## http://www.pubnub.com/
7
7
 
8
8
  ## -----------------------------------
9
- ## PubNub 3.1 Real-time Push Cloud API
9
+ ## PubNub 3.3 Real-time Push Cloud API
10
10
  ## -----------------------------------
11
11
 
12
- ## including required libraries
13
- require 'openssl'
14
12
  require 'base64'
15
13
  require 'open-uri'
16
14
  require 'uri'
17
- require 'net/http'
18
- require 'net/https'
19
- require 'json'
20
- require 'pp'
21
- require 'rubygems'
22
- require 'securerandom'
23
- require 'digest'
15
+
24
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 3.1 with Cipher Key
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 )
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
-
48
- if @ssl
49
- @origin = 'https://' + @origin
29
+
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
+
50
66
  else
51
- @origin = 'http://' + @origin
67
+ raise(InitError, "Initialize with either a hash of options, or exactly 5 named parameters.")
52
68
  end
69
+
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)
53
99
  end
54
100
 
55
- #**
56
- #* Publish
57
- #*
58
- #* Send a message to a channel.
59
- #*
60
- #* @param array args with channel and message.
61
- #* @return array success information.
62
- #*
63
- def publish(args)
64
- ## Fail if bad input.
65
- if !(args['channel'] && args['message'] && args['callback'])
66
- puts('Missing Channel or Message or Callback')
67
- return false
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)
68
128
  end
69
129
 
70
- ## Capture User Input
71
- channel = args['channel']
72
- message = args['message']
73
- callback = args['callback']
74
-
75
- # Encryption of message
76
- if @cipher_key.length > 0
77
- pc=PubnubCrypto.new(@cipher_key)
78
- if message.is_a? Array
79
- message=pc.encryptArray(message)
80
- else
81
- message=pc.encryptObject(message)
82
- end
83
- else
84
- message = args['message'].to_json();
130
+ options = HashWithIndifferentAccess.new(options) unless (options == nil)
131
+
132
+ unless options[:channel] && options[:callback]
133
+ raise(ArgumentError, usage_error)
134
+ end
135
+
136
+ subscribe(options.merge(:operation => "presence"))
137
+
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)
85
145
  end
86
146
 
87
- ## Sign message using HMAC
88
- String signature = '0'
89
- if @secret_key.length > 0
90
- signature = "{@publish_key,@subscribe_key,@secret_key,channel,message}"
91
- digest = OpenSSL::Digest.new("sha256")
92
- key = [ @secret_key ]
93
- hmac = OpenSSL::HMAC.hexdigest(digest, key.pack("H*"), signature)
94
- signature = hmac
147
+ options = HashWithIndifferentAccess.new(options) unless (options == nil)
148
+
149
+ unless options[:channel] && options[:callback]
150
+ raise(ArgumentError, usage_error)
95
151
  end
96
152
 
97
- ## Send Message
98
- request = [ 'publish', @publish_key, @subscribe_key, signature, channel, '0', message ]
99
- args['request'] = request
100
- args['callback'] = callback
101
- _request(args)
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
+
102
164
  end
103
165
 
104
- #**
105
- #* Subscribe
106
- #*
107
- #* This is NON-BLOCKING.
108
- #* Listen for a message on a channel.
109
- #*
110
- #* @param array args with channel and message.
111
- #* @return false on fail, array on success.
112
- #*
113
- def subscribe(args)
114
- ## Capture User Input
115
- channel = args['channel']
116
- callback = args['callback']
117
-
118
- ## Fail if missing channel
119
- if !channel
120
- puts "Missing Channel."
121
- return false
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)
122
170
  end
123
171
 
124
- ## Fail if missing callback
125
- if !callback
126
- puts "Missing Callback."
127
- return false
172
+ options = HashWithIndifferentAccess.new(options) unless (options == nil)
173
+
174
+ unless options[:count] && options[:channel] && options[:callback]
175
+ raise(ArgumentError, usage_error)
128
176
  end
129
177
 
130
- ## EventMachine loop
131
- #EventMachine.run do
132
- timetoken = 0
133
- request = [ 'subscribe', @subscribe_key, channel, '0', timetoken.to_s ]
134
- args['request'] = request
135
- _subscribe(args)
136
- #end
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
+
137
200
  end
138
201
 
139
- #**
140
- #* History
141
- #*
142
- #* Load history from a channel.
143
- #*
144
- #* @param array args with 'channel' and 'limit'.
145
- #* @return mixed false on fail, array on success.
146
- #*
147
- def history(args)
148
- ## Capture User Input
149
- limit = +args['limit'] ? +args['limit'] : 5
150
- channel = args['channel']
151
- callback = args['callback']
152
-
153
- ## Fail if bad input.
154
- if (!channel)
155
- puts 'Missing Channel.'
156
- return false
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)
157
206
  end
158
- if (!callback)
159
- puts 'Missing Callback.'
160
- return false
207
+
208
+ options = HashWithIndifferentAccess.new(options) unless (options == nil)
209
+
210
+ unless options[:limit] && options[:channel] && options[:callback]
211
+ raise(ArgumentError, usage_error)
161
212
  end
162
213
 
163
- ## Get History
164
- request = [ 'history', @subscribe_key, channel, '0', limit.to_s ]
165
- args['request'] = request
166
- _request(args)
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
+
167
232
  end
168
233
 
169
- #**
170
- #* Time
171
- #*
172
- #* Timestamp from PubNub Cloud.
173
- #*
174
- #* @return int timestamp.
175
- #*
176
- def time(args)
177
- request = [ 'time', '0' ]
178
- args['request'] = request
179
- _request(args)
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
+ ""
250
+ end
251
+
180
252
  end
181
253
 
182
- #**
183
- #* UUID
184
- #*
185
- #* Unique identifier generation
186
- #*
187
- #* @return Unique Identifier
188
- #*
189
- def UUID()
190
- uuid=SecureRandom.base64(32).gsub("/","_").gsub(/=+$/,"")
254
+ def uuid
255
+ UUID.new.generate
191
256
  end
192
257
 
193
- private
194
-
195
- #**
196
- #* Request URL for subscribe
197
- #*
198
- #* @param array request of url directories.
199
- #* @return array from JSON response.
200
- #*
201
- def _subscribe(args)
202
- channel = args['channel']
203
- callback = args['callback']
204
- request = args['request']
205
-
206
- # Construct Request
207
- url = encode_URL(request);
208
- url = @origin + url
209
-
210
- # Execute Request
211
- open(url) do |f|
212
- http_response = JSON.parse(f.read)
213
- messages = http_response[0]
214
- timetoken = http_response[1]
215
-
216
- next if !messages.length
217
-
218
- ## Run user Callback and Reconnect if user permits.
219
- ## Capture the message and encrypt it
220
- if @cipher_key.length > 0
221
- pc = PubnubCrypto.new(@cipher_key)
222
- messages.each do |message|
223
- if message.is_a? Array
224
- message=pc.decryptArray(message)
225
- else
226
- message=pc.decryptObject(message)
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}")
272
+
273
+ begin
274
+
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)
227
284
  end
228
- if !callback.call(message)
229
- return
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
230
296
  end
231
- end
232
- else
233
- messages.each do |message|
234
- if !callback.call(message)
235
- return
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)
236
306
  end
237
- end
238
- end
239
307
 
240
- request = [ 'subscribe', @subscribe_key, channel, '0', timetoken.to_s ]
241
- args['request'] = request
242
- # Recusive call to _subscribe
243
- _subscribe(args)
244
- end
245
- end
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
246
325
 
247
- #**
248
- #* Request URL
249
- #*
250
- #* @param array request of url directories.
251
- #* @return array from JSON response.
252
- #*
253
- def _request(args)
254
- request = args['request']
255
- callback = args['callback']
256
- url = encode_URL(request)
257
- url = @origin + url
258
- open(url) do |f|
259
- response = JSON.parse(f.read)
260
- if request[0] == 'history'
261
- if @cipher_key.length > 0
262
- myarr=Array.new()
263
- response.each do |message|
264
- pc=PubnubCrypto.new(@cipher_key)
265
- if message.is_a? Array
266
- message=pc.decryptArray(message)
326
+ timeout_timer.cancel
327
+ error_timer.cancel
328
+
329
+ _request(request)
267
330
  else
268
- message=pc.decryptObject(message)
331
+ conn.close_connection
332
+ return request.response
333
+
269
334
  end
270
- myarr.push(message)
335
+
271
336
  end
272
- callback.call(myarr)
273
- else
274
- callback.call(response)
275
337
  end
276
- elsif request[0] == 'publish'
277
- callback.call(response)
278
- else
279
- callback.call(response)
280
338
  end
339
+
340
+ rescue EventMachine::ConnectionError => e
341
+ error_message = "Network Error: #{e.message}"
342
+ puts(error_message)
343
+ return [0, error_message]
281
344
  end
345
+
346
+ end
282
347
  end
283
348
 
284
- def encode_URL(request)
285
- ## Construct Request URL
286
- url = '/' + request.map{ |bit| bit.split('').map{ |ch|
287
- ' ~`!@#$%^&*()+=[]\\{}|;\':",./<>?'.index(ch) ?
288
- '%' + ch.unpack('H2')[0].to_s.upcase : URI.encode(ch)
289
- }.join('') }.join('/')
290
- return url
349
+ def reconnect_and_query(conn, request)
350
+ conn.reconnect request.host, request.port
351
+ conn.pubnub_request = request
352
+ conn.get(request.query)
291
353
  end
354
+
292
355
  end