sockudo 1.0.0 → 2.0.0

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.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'base64'
2
4
  require 'securerandom'
3
5
  require 'pusher-signature'
@@ -14,7 +16,7 @@ module Sockudo
14
16
  DEFAULT_SEND_TIMEOUT = 5
15
17
  DEFAULT_RECEIVE_TIMEOUT = 5
16
18
  DEFAULT_KEEP_ALIVE_TIMEOUT = 30
17
- DEFAULT_CLUSTER = "mt1"
19
+ DEFAULT_CLUSTER = 'mt1'
18
20
 
19
21
  # Loads the configuration from an url in the environment
20
22
  def self.from_env(key = 'SOCKUDO_URL')
@@ -30,15 +32,15 @@ module Sockudo
30
32
  end
31
33
 
32
34
  def initialize(options = {})
33
- @scheme = "https"
35
+ @scheme = 'https'
34
36
  @port = options[:port] || 443
35
37
 
36
38
  if options.key?(:encrypted)
37
- warn "[DEPRECATION] `encrypted` is deprecated and will be removed in the next major version. Use `use_tls` instead."
39
+ warn '[DEPRECATION] `encrypted` is deprecated and will be removed in the next major version. Use `use_tls` instead.'
38
40
  end
39
41
 
40
42
  if options[:use_tls] == false || options[:encrypted] == false
41
- @scheme = "http"
43
+ @scheme = 'http'
42
44
  @port = options[:port] || 80
43
45
  end
44
46
 
@@ -69,18 +71,20 @@ module Sockudo
69
71
  def authentication_token
70
72
  raise ConfigurationError, :key unless @key
71
73
  raise ConfigurationError, :secret unless @secret
72
- Sockudo::Signature::Token.new(@key, @secret)
74
+
75
+ Pusher::Signature::Token.new(@key, @secret)
73
76
  end
74
77
 
75
78
  # @private Builds a url for this app, optionally appending a path
76
79
  def url(path = nil)
77
80
  raise ConfigurationError, :app_id unless @app_id
81
+
78
82
  URI::Generic.build({
79
- scheme: @scheme,
80
- host: @host,
81
- port: @port,
82
- path: "/apps/#{@app_id}#{path}"
83
- })
83
+ scheme: @scheme,
84
+ host: @host,
85
+ port: @port,
86
+ path: "/apps/#{@app_id}#{path}"
87
+ })
84
88
  end
85
89
 
86
90
  # Configure Sockudo connection by providing a url rather than specifying
@@ -136,13 +140,15 @@ module Sockudo
136
140
  # Convenience method to set all timeouts to the same value (in seconds).
137
141
  # For more control, use the individual writers.
138
142
  def timeout=(value)
139
- @connect_timeout, @send_timeout, @receive_timeout = value, value, value
143
+ @connect_timeout = value
144
+ @send_timeout = value
145
+ @receive_timeout = value
140
146
  end
141
147
 
142
148
  # Set an encryption_master_key to use with private-encrypted channels from
143
149
  # a base64 encoded string.
144
- def encryption_master_key_base64=(s)
145
- @encryption_master_key = s ? Base64.strict_decode64(s) : nil
150
+ def encryption_master_key_base64=(str)
151
+ @encryption_master_key = str ? Base64.strict_decode64(str) : nil
146
152
  end
147
153
 
148
154
  ## INTERACT WITH THE API ##
@@ -169,8 +175,8 @@ module Sockudo
169
175
  # @raise [Sockudo::Error] Unsuccessful response - see the error message
170
176
  # @raise [Sockudo::HTTPError] Error raised inside http client. The original error is wrapped in error.original_error
171
177
  #
172
- def get(path, params = {})
173
- resource(path).get(params)
178
+ def get(path, params = {}, headers = {})
179
+ resource(path).get(params, headers)
174
180
  end
175
181
 
176
182
  # GET arbitrary REST API resource using an asynchronous http client.
@@ -185,8 +191,8 @@ module Sockudo
185
191
  #
186
192
  # @return Either an EM::DefaultDeferrable or a HTTPClient::Connection
187
193
  #
188
- def get_async(path, params = {})
189
- resource(path).get_async(params)
194
+ def get_async(path, params = {}, headers = {})
195
+ resource(path).get_async(params, headers)
190
196
  end
191
197
 
192
198
  # POST arbitrary REST API resource using a synchronous http client.
@@ -195,6 +201,12 @@ module Sockudo
195
201
  resource(path).post(params, headers)
196
202
  end
197
203
 
204
+ # DELETE arbitrary REST API resource using a synchronous http client.
205
+ # All request signing is handled automatically.
206
+ def delete(path, params = {}, headers = {})
207
+ resource(path).delete(params, headers)
208
+ end
209
+
198
210
  # POST arbitrary REST API resource using an asynchronous http client.
199
211
  # Works identially to get_async method, but posts params as JSON in post
200
212
  # body.
@@ -227,7 +239,7 @@ module Sockudo
227
239
  Channel.new(nil, channel_name, self)
228
240
  end
229
241
 
230
- alias :[] :channel
242
+ alias [] channel
231
243
 
232
244
  # Request a list of occupied channels from the API
233
245
  #
@@ -260,6 +272,91 @@ module Sockudo
260
272
  get("/channels/#{channel_name}", params)
261
273
  end
262
274
 
275
+ # Request durable history for a specific channel
276
+ #
277
+ # GET /apps/[id]/channels/[channel_name]/history
278
+ #
279
+ # @param channel_name [String] Channel name (max 200 characters)
280
+ # @param params [Hash] Hash of parameters for the API. Supported keys include:
281
+ # :limit, :direction, :cursor, :start_serial, :end_serial, :start_time_ms, :end_time_ms
282
+ #
283
+ # @return [Hash] See Sockudo history API docs
284
+ #
285
+ # @raise [Sockudo::Error] Unsuccessful response - see the error message
286
+ # @raise [Sockudo::HTTPError] Error raised inside http client. The original error is wrapped in error.original_error
287
+ #
288
+ def channel_history(channel_name, params = {})
289
+ get("/channels/#{channel_name}/history", params)
290
+ end
291
+
292
+ # Request presence history for a specific presence channel
293
+ #
294
+ # GET /apps/[id]/channels/[channel_name]/presence/history
295
+ #
296
+ # @param channel_name [String] Presence channel name (max 200 characters)
297
+ # @param params [Hash] Hash of parameters for the API. Supported keys include:
298
+ # :limit, :direction, :cursor, :start_serial, :end_serial, :start_time_ms, :end_time_ms
299
+ #
300
+ # @return [Hash] See Sockudo presence history API docs
301
+ #
302
+ def channel_presence_history(channel_name, params = {})
303
+ get("/channels/#{channel_name}/presence/history", params)
304
+ end
305
+
306
+ # Request a reconstructed presence snapshot for a specific presence channel
307
+ #
308
+ # GET /apps/[id]/channels/[channel_name]/presence/history/snapshot
309
+ #
310
+ # @param channel_name [String] Presence channel name (max 200 characters)
311
+ # @param params [Hash] Hash of parameters for the API. Supported keys include:
312
+ # :at_time_ms, :at_serial
313
+ #
314
+ # @return [Hash] See Sockudo presence snapshot API docs
315
+ #
316
+ def channel_presence_snapshot(channel_name, params = {})
317
+ get("/channels/#{channel_name}/presence/history/snapshot", params)
318
+ end
319
+
320
+ # Request the latest visible version of a mutable message
321
+ def get_message(channel_name, message_serial, params = {})
322
+ get("/channels/#{channel_name}/messages/#{message_serial}", params)
323
+ end
324
+
325
+ # Request preserved versions of a mutable message
326
+ def get_message_versions(channel_name, message_serial, params = {})
327
+ get("/channels/#{channel_name}/messages/#{message_serial}/versions", params)
328
+ end
329
+
330
+ # Apply a mutable-message update
331
+ def update_message(channel_name, message_serial, params = {})
332
+ post("/channels/#{channel_name}/messages/#{message_serial}/update", params)
333
+ end
334
+
335
+ # Apply a mutable-message delete
336
+ def delete_message(channel_name, message_serial, params = {})
337
+ post("/channels/#{channel_name}/messages/#{message_serial}/delete", params)
338
+ end
339
+
340
+ # Apply a mutable-message append
341
+ def append_message(channel_name, message_serial, params = {})
342
+ post("/channels/#{channel_name}/messages/#{message_serial}/append", params)
343
+ end
344
+
345
+ # Publish an annotation for a versioned message
346
+ def publish_annotation(channel_name, message_serial, params = {})
347
+ post("/channels/#{channel_name}/messages/#{message_serial}/annotations", params)
348
+ end
349
+
350
+ # Delete an annotation from a versioned message
351
+ def delete_annotation(channel_name, message_serial, annotation_serial, params = {})
352
+ delete("/channels/#{channel_name}/messages/#{message_serial}/annotations/#{annotation_serial}", params)
353
+ end
354
+
355
+ # List raw annotation events for a versioned message
356
+ def list_annotations(channel_name, message_serial, params = {})
357
+ get("/channels/#{channel_name}/messages/#{message_serial}/annotations", params)
358
+ end
359
+
263
360
  # Request info for users of a presence channel
264
361
  #
265
362
  # GET /apps/[id]/channels/[channel_name]/users
@@ -276,6 +373,114 @@ module Sockudo
276
373
  get("/channels/#{channel_name}/users", params)
277
374
  end
278
375
 
376
+ # Activate or create a push device registration with admin scope
377
+ def activate_device(device, options = {})
378
+ post(push_path('/deviceRegistrations'), device,
379
+ push_headers('push-admin', nil, rotate_device_identity_token: options[:rotate_device_identity_token]))
380
+ end
381
+
382
+ # Alias of activate_device
383
+ def create_device_activation(device, options = {})
384
+ activate_device(device, options)
385
+ end
386
+
387
+ # Update a push device registration with push-subscribe scope
388
+ def update_device_registration(device, device_identity_token)
389
+ post(push_path('/deviceRegistrations'), device, push_headers('push-subscribe', device_identity_token))
390
+ end
391
+
392
+ # List push device registrations with cursor pagination
393
+ def list_device_registrations(params = {})
394
+ get(push_path('/deviceRegistrations'), params, push_headers('push-admin'))
395
+ end
396
+
397
+ # Get a push device registration
398
+ def get_device_registration(device_id, device_identity_token = nil)
399
+ capability = device_identity_token ? 'push-subscribe' : 'push-admin'
400
+ get(push_path("/deviceRegistrations/#{device_id}"), {}, push_headers(capability, device_identity_token))
401
+ end
402
+
403
+ # Delete a push device registration
404
+ def delete_device_registration(device_id, device_identity_token = nil)
405
+ capability = device_identity_token ? 'push-subscribe' : 'push-admin'
406
+ delete(push_path("/deviceRegistrations/#{device_id}"), {}, push_headers(capability, device_identity_token))
407
+ end
408
+
409
+ # Delete all device registrations for a client identifier
410
+ def remove_device_registrations_by_client(client_id)
411
+ delete(push_path('/deviceRegistrations'), { clientId: client_id }, push_headers('push-admin'))
412
+ end
413
+
414
+ # Upsert a push channel subscription
415
+ def upsert_channel_push_subscription(subscription, device_identity_token = nil)
416
+ capability = device_identity_token ? 'push-subscribe' : 'push-admin'
417
+ post(push_path('/channelSubscriptions'), subscription, push_headers(capability, device_identity_token))
418
+ end
419
+
420
+ # List push channel subscriptions with cursor pagination
421
+ def list_channel_push_subscriptions(params = {}, device_identity_token = nil)
422
+ capability = device_identity_token ? 'push-subscribe' : 'push-admin'
423
+ get(push_path('/channelSubscriptions'), params, push_headers(capability, device_identity_token))
424
+ end
425
+
426
+ # Delete push channel subscriptions
427
+ def delete_channel_push_subscriptions(params = {}, device_identity_token = nil)
428
+ capability = device_identity_token ? 'push-subscribe' : 'push-admin'
429
+ delete(push_path('/channelSubscriptions'), params, push_headers(capability, device_identity_token))
430
+ end
431
+
432
+ # List subscribed channels with cursor pagination
433
+ def list_channel_push_subscription_channels(params = {})
434
+ get(push_path('/channelSubscriptions/channels'), params, push_headers('push-admin'))
435
+ end
436
+
437
+ # List stored push provider credentials with cursor pagination
438
+ def list_push_credentials(params = {})
439
+ get(push_path('/credentials'), params, push_headers('push-admin'))
440
+ end
441
+
442
+ # Store or update a provider credential payload
443
+ def put_push_credential(provider, credential)
444
+ post(push_path("/credentials/#{provider}"), credential, push_headers('push-admin'))
445
+ end
446
+
447
+ # Publish push asynchronously by default
448
+ def publish_push(request)
449
+ post(push_path('/publish'), request.merge(sync: false), push_headers('push-admin'))
450
+ end
451
+
452
+ # Alias of publish_push
453
+ def publish_push_direct(request)
454
+ publish_push(request)
455
+ end
456
+
457
+ # Publish a batch of push notifications asynchronously by default
458
+ def publish_push_batch(requests)
459
+ post(push_path('/batch/publish'), requests.map { |request| request.merge(sync: false) }, push_headers('push-admin'))
460
+ end
461
+
462
+ # Schedule a push publish; requires notBeforeMs in the request
463
+ def schedule_push(request)
464
+ raise Sockudo::Error, 'scheduled push requires notBeforeMs' unless request.key?(:notBeforeMs) || request.key?('notBeforeMs')
465
+
466
+ publish_push(request)
467
+ end
468
+
469
+ # Get the status for a publish id
470
+ def get_publish_status(publish_id)
471
+ get(push_path("/publish/#{publish_id}/status"), {}, push_headers('push-admin'))
472
+ end
473
+
474
+ # Cancel a scheduled publish
475
+ def cancel_scheduled_push(publish_id)
476
+ delete(push_path("/scheduled/#{publish_id}"), {}, push_headers('push-admin'))
477
+ end
478
+
479
+ # Submit a provider delivery status event
480
+ def post_push_delivery_status(event)
481
+ post(push_path('/deliveryStatus'), event, push_headers('push-admin'))
482
+ end
483
+
279
484
  # Trigger an event on one or more channels
280
485
  #
281
486
  # POST /apps/[app_id]/events
@@ -334,7 +539,6 @@ module Sockudo
334
539
  post_async('/batch_events', trigger_batch_params(flat_events))
335
540
  end
336
541
 
337
-
338
542
  # Generate the expected response for an authentication endpoint.
339
543
  # See https://sockudo.com/docs/channels/server_api/authorizing-users for details.
340
544
  #
@@ -399,42 +603,37 @@ module Sockudo
399
603
  def sync_http_client
400
604
  require 'httpclient'
401
605
 
402
- @client ||= begin
403
- HTTPClient.new(@http_proxy).tap do |c|
404
- c.connect_timeout = @connect_timeout
405
- c.send_timeout = @send_timeout
406
- c.receive_timeout = @receive_timeout
407
- c.keep_alive_timeout = @keep_alive_timeout
408
- end
606
+ @sync_http_client ||= HTTPClient.new(@http_proxy).tap do |c|
607
+ c.connect_timeout = @connect_timeout
608
+ c.send_timeout = @send_timeout
609
+ c.receive_timeout = @receive_timeout
610
+ c.keep_alive_timeout = @keep_alive_timeout
409
611
  end
410
612
  end
411
613
 
412
614
  # @private Construct an em-http-request http client
413
615
  def em_http_client(uri)
414
- begin
415
- unless defined?(EventMachine) && EventMachine.reactor_running?
416
- raise Error, "In order to use async calling you must be running inside an eventmachine loop"
417
- end
418
- require 'em-http' unless defined?(EventMachine::HttpRequest)
616
+ unless defined?(EventMachine) && EventMachine.reactor_running?
617
+ raise Error, 'In order to use async calling you must be running inside an eventmachine loop'
618
+ end
419
619
 
420
- connection_opts = {
421
- connect_timeout: @connect_timeout,
422
- inactivity_timeout: @receive_timeout,
423
- }
620
+ require 'em-http' unless defined?(EventMachine::HttpRequest)
424
621
 
425
- if defined?(@proxy)
426
- proxy_opts = {
427
- host: @proxy[:host],
428
- port: @proxy[:port]
429
- }
430
- if @proxy[:user]
431
- proxy_opts[:authorization] = [@proxy[:user], @proxy[:password]]
432
- end
433
- connection_opts[:proxy] = proxy_opts
434
- end
622
+ connection_opts = {
623
+ connect_timeout: @connect_timeout,
624
+ inactivity_timeout: @receive_timeout
625
+ }
435
626
 
436
- EventMachine::HttpRequest.new(uri, connection_opts)
627
+ if defined?(@proxy)
628
+ proxy_opts = {
629
+ host: @proxy[:host],
630
+ port: @proxy[:port]
631
+ }
632
+ proxy_opts[:authorization] = [@proxy[:user], @proxy[:password]] if @proxy[:user]
633
+ connection_opts[:proxy] = proxy_opts
437
634
  end
635
+
636
+ EventMachine::HttpRequest.new(uri, connection_opts)
438
637
  end
439
638
 
440
639
  private
@@ -443,16 +642,19 @@ module Sockudo
443
642
 
444
643
  def inject_auto_idempotency_key(params)
445
644
  return params if params.key?(:idempotency_key) || !@auto_idempotency_key
645
+
446
646
  serial = (@publish_serial += 1)
447
647
  params.merge(idempotency_key: "#{@base_id}:#{serial}")
448
648
  end
449
649
 
450
650
  def inject_auto_idempotency_keys_batch!(events)
451
651
  return false unless @auto_idempotency_key
652
+
452
653
  serial = (@publish_serial += 1)
453
654
  injected = false
454
655
  events.each_with_index do |event, index|
455
656
  next if event.key?(:idempotency_key)
657
+
456
658
  event[:idempotency_key] = "#{@base_id}:#{serial}:#{index}"
457
659
  injected = true
458
660
  end
@@ -462,19 +664,15 @@ module Sockudo
462
664
  def post_with_retry(path, body, headers = {})
463
665
  last_error = nil
464
666
  @max_retries.times do |attempt|
465
- begin
466
- return post(path, body, headers)
467
- rescue Sockudo::HTTPError => e
468
- last_error = e
469
- raise unless attempt < @max_retries - 1
470
- rescue Sockudo::Error => e
471
- if e.respond_to?(:status) && e.status.is_a?(Integer) && e.status >= 500 && e.status < 600
472
- last_error = e
473
- raise unless attempt < @max_retries - 1
474
- else
475
- raise
476
- end
477
- end
667
+ return post(path, body, headers)
668
+ rescue Sockudo::HTTPError => e
669
+ last_error = e
670
+ raise unless attempt < @max_retries - 1
671
+ rescue Sockudo::Error => e
672
+ raise unless e.respond_to?(:status) && e.status.is_a?(Integer) && e.status >= 500 && e.status < 600
673
+
674
+ last_error = e
675
+ raise unless attempt < @max_retries - 1
478
676
  end
479
677
  raise last_error
480
678
  end
@@ -483,18 +681,22 @@ module Sockudo
483
681
  channels = Array(channels).map(&:to_s)
484
682
  raise Sockudo::Error, "Too many channels (#{channels.length}), max 100" if channels.length > 100
485
683
 
486
- encoded_data = if channels.any?{ |c| c.match(/^private-encrypted-/) } then
487
- raise Sockudo::Error, "Cannot trigger to multiple channels if any are encrypted" if channels.length > 1
488
- encrypt(channels[0], encode_data(data))
489
- else
490
- encode_data(data)
491
- end
684
+ encoded_data = if channels.any? { |c| c.match(/^private-encrypted-/) }
685
+ if channels.length > 1
686
+ raise Sockudo::Error,
687
+ 'Cannot trigger to multiple channels if any are encrypted'
688
+ end
689
+
690
+ encrypt(channels[0], encode_data(data))
691
+ else
692
+ encode_data(data)
693
+ end
492
694
 
493
695
  params.merge({
494
- name: event_name,
495
- channels: channels,
496
- data: encoded_data,
497
- })
696
+ name: event_name,
697
+ channels: channels,
698
+ data: encoded_data
699
+ })
498
700
  end
499
701
 
500
702
  def trigger_params_with_headers(channels, event_name, data, params)
@@ -513,11 +715,11 @@ module Sockudo
513
715
  {
514
716
  batch: events.map do |event|
515
717
  event.dup.tap do |e|
516
- e[:data] = if e[:channel].match(/^private-encrypted-/) then
517
- encrypt(e[:channel], encode_data(e[:data]))
518
- else
519
- encode_data(e[:data])
520
- end
718
+ e[:data] = if e[:channel].match(/^private-encrypted-/)
719
+ encrypt(e[:channel], encode_data(e[:data]))
720
+ else
721
+ encode_data(e[:data])
722
+ end
521
723
  end
522
724
  end
523
725
  }
@@ -526,6 +728,7 @@ module Sockudo
526
728
  # JSON-encode the data if it's not a string
527
729
  def encode_data(data)
528
730
  return data if data.is_a? String
731
+
529
732
  MultiJson.encode(data)
530
733
  end
531
734
 
@@ -546,9 +749,9 @@ module Sockudo
546
749
  ciphertext = secret_box.encrypt(nonce, encoded_data)
547
750
 
548
751
  MultiJson.encode({
549
- "nonce" => Base64::strict_encode64(nonce),
550
- "ciphertext" => Base64::strict_encode64(ciphertext),
551
- })
752
+ 'nonce' => Base64.strict_encode64(nonce),
753
+ 'ciphertext' => Base64.strict_encode64(ciphertext)
754
+ })
552
755
  end
553
756
 
554
757
  def configured?
@@ -558,7 +761,7 @@ module Sockudo
558
761
  def require_rbnacl
559
762
  require 'rbnacl'
560
763
  rescue LoadError => e
561
- $stderr.puts "You don't have rbnacl installed in your application. Please add it to your Gemfile and run bundle install"
764
+ warn "You don't have rbnacl installed in your application. Please add it to your Gemfile and run bundle install"
562
765
  raise e
563
766
  end
564
767
 
@@ -575,7 +778,7 @@ module Sockudo
575
778
  # @raise [Sockudo::Error] if socket_id or custom_string invalid
576
779
  #
577
780
  def authentication_string(socket_id, custom_string = nil)
578
- string_to_sign = [socket_id, 'user', custom_string].compact.map(&:to_s).join('::')
781
+ string_to_sign = [socket_id, 'user', custom_string].compact.join('::')
579
782
 
580
783
  _authentication_string(socket_id, string_to_sign, authentication_token, string_to_sign)
581
784
  end
@@ -589,5 +792,16 @@ module Sockudo
589
792
  def user_data_valid?(data)
590
793
  data.is_a?(Hash) && data.key?(:id) && !data[:id].empty? && data[:id].is_a?(String)
591
794
  end
795
+
796
+ def push_headers(capability = 'push-admin', device_identity_token = nil, rotate_device_identity_token: false)
797
+ headers = { 'X-Sockudo-Push-Capability' => capability }
798
+ headers['X-Sockudo-Device-Identity-Token'] = device_identity_token if device_identity_token
799
+ headers['X-Sockudo-Rotate-Device-Identity-Token'] = 'true' if rotate_device_identity_token
800
+ headers
801
+ end
802
+
803
+ def push_path(path)
804
+ "/push#{path}"
805
+ end
592
806
  end
593
807
  end