pubnub-ruby 0.0.9 → 3.3.0.1

Sign up to get free protection for your applications and to get access to all the features.
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