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 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) 2010 Stephen Blum
5
+ ## Copyright (c) 2012 PubNub
6
6
  ## http://www.pubnub.com/
7
7
 
8
8
  ## -----------------------------------
9
- ## PubNub 3.0 Real-time Push Cloud API
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
- require 'net/http'
19
- require 'json'
20
- require 'pp'
21
- require 'rubygems'
22
- require 'securerandom'
23
- require 'digest'
24
- require './lib/PubnubCrypto.rb'
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
- uri = URI.parse(@origin)
56
- http = Net::HTTP.new(uri.host, uri.port)
57
- http.use_ssl = ssl_on
58
- @connection = http.start()
59
-
60
- puts('Connection done')
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
- #* UUID
66
- #*
67
- #* Unique identifier generation
68
- #* @Return Unique Identifier
69
- #*
70
- def UUID()
71
- uuid=SecureRandom.base64(32).gsub("/","_").gsub(/=+$/,"")
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
- ## Capture User Input
91
- channel = args['channel']
92
- message = args['message'].to_json
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
- ## Send message
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
- ## Fail if missing callback
157
- if !callback
158
- puts "Missing Callback."
159
- return false
160
- end
161
-
162
- ## Begin Subscribe
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
- ## Get History
216
- response = self._request([ 'history', @subscribe_key,channel,'0',limit.to_s])
217
- myarr=Array.new()
218
- response.each do |message|
219
- pc = PubnubCrypto.new('cipher_key')
220
- message = pc.decrypt(message)
221
- myarr.push(message)
222
- end
223
- return myarr
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
- #* Time
227
- #*
228
- #* Timestamp from PubNub Cloud.
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
- #* Request URL
240
- #*
241
- #* @param array request of url directories.
242
- #* @return array from JSON response.
243
- #*
244
- def _request(request)
245
- ## Construct Request URL
246
- url = '/' + request.map{ |bit| bit.split('').map{ |ch|
247
- ' ~`!@#$%^&*()+=[]\\{}|;\':",./<>?'.index(ch) ?
248
- '%' + ch.unpack('H2')[0].to_s.upcase : URI.encode(ch)
249
- }.join('') }.join('/')
250
-
251
- puts(' URL==> '+url)
252
-
253
- response = send_with_retries(url,MAX_RETRIES )
254
- JSON.parse(response)
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
- private
258
- def send_with_retries(url, retries)
259
- tries = 0
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
- tries += 1
266
- tries < retries ? retry : raise
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