ruby-push-notifications 1.1.0 → 1.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 233dc6f0f402cb52394547c792773699f75c920a
4
- data.tar.gz: 375783019646d00e93261ef0b032fa59547a4ab6
3
+ metadata.gz: d47dcca6cd9dfae155c566c9b2b90ba334e5e3f5
4
+ data.tar.gz: 957bc60afb47661d910e50f771c46ea108dbe7df
5
5
  SHA512:
6
- metadata.gz: f5bbf5568795d0026087b4e84a6856cf1de77738c1cac3daae2f136f9e16f221734b1becf2304d5ea24104cece5820926407b152a5b1a02290595dd4c4b163fe
7
- data.tar.gz: 2f5a20d5a059bbf43c9116d3eb8af2cd4d5b5f00435cc6c674de1bf215746f7926742f6a92b30ac8863ed1a8769c0597e13e882de464cb1947d66f526ab410a0
6
+ metadata.gz: e0c21ac5060259358f3a304934cc562e7d959b7dd3894ce8aaaf87843a49cbf3a5764e450d48f606db1469bafb6f01a423cacabf4d20300e34433f15db550190
7
+ data.tar.gz: a012744bfd5eab89450238c778786a84a6bde7df4a7ccd406d0a0fc0bb3f7b53443c8e41708d05b52621b5678b93a136a99d741e8c1a8accaf7b36413969f51b
@@ -3,6 +3,11 @@ All notable changes to this project will be documented in this file.
3
3
  This project adheres to [Semantic Versioning](http://semver.org/).
4
4
 
5
5
  ## [unrealeased] - [unrealease date]
6
+
7
+ ## 1.2.0 - 2017-09-20
8
+ - **Added**: Add support Windows Push Notification Services (WNS)
9
+
10
+ ## 1.1.0 - 2017-03-14
6
11
  - **Added**: Option (`url`) to set custom GCM endpoint (Android)
7
12
  - **Added**: Option (`host`) to set custom APNS environment (iOS)
8
13
 
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![Build Status](https://travis-ci.org/calonso/ruby-push-notifications.svg)](https://travis-ci.org/calonso/ruby-push-notifications) [![Dependency Status](https://gemnasium.com/calonso/ruby-push-notifications.svg)](https://gemnasium.com/calonso/ruby-push-notifications) [![Code Climate](https://codeclimate.com/github/calonso/ruby-push-notifications/badges/gpa.svg)](https://codeclimate.com/github/calonso/ruby-push-notifications) [![Test Coverage](https://codeclimate.com/github/calonso/ruby-push-notifications/badges/coverage.svg)](https://codeclimate.com/github/calonso/ruby-push-notifications) [![Gem Version](https://badge.fury.io/rb/ruby-push-notifications.svg)](http://badge.fury.io/rb/ruby-push-notifications) [![Join the chat at https://gitter.im/calonso/ruby-push-notifications](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/calonso/ruby-push-notifications?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
4
4
 
5
- ###iOS, Android and Windows Phone Push Notifications made easy!
5
+ ### iOS, Android and Windows Phone Push Notifications made easy!
6
6
 
7
7
  ## Features
8
8
 
@@ -38,7 +38,8 @@ For completely detailed examples:
38
38
 
39
39
  1. [Apple iOS example](https://github.com/calonso/ruby-push-notifications/tree/master/examples/apns.rb)
40
40
  2. [Google Android example](https://github.com/calonso/ruby-push-notifications/tree/master/examples/gcm.rb)
41
- 3. [Windows Phone example](https://github.com/calonso/ruby-push-notifications/tree/master/examples/mpns.rb)
41
+ 3. [Windows Phone(MPNS) example](https://github.com/calonso/ruby-push-notifications/tree/master/examples/mpns.rb)
42
+ 4. [Windows Phone(WNS) example](https://github.com/calonso/ruby-push-notifications/tree/master/examples/wns.rb)
42
43
 
43
44
  ## Pending tasks
44
45
 
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
4
+
5
+ require 'ruby-push-notifications'
6
+
7
+ sid = "ms-app://...."
8
+ secret = "..."
9
+
10
+ wns_access = RubyPushNotifications::WNS::WNSAccess.new(sid, secret).get_token
11
+ access_token = wns_access.response.access_token
12
+
13
+ device_urls = ["https://hk2.notify.windows.com/?token=AwYAAACEIPGNfZtVWJMvU%2fJ67TTdLQNdFl2345SbaCP7dmkvUmy6Cv9Y7hM4UT5uA%2b3obnZlIIhQcse9Hs6RPW0MPFVjf3MugnfD8qjIHMtRKIsKENIFveN1xQ6S38m90%2fgaBsE%3d"]
14
+
15
+ notification = RubyPushNotifications::WNS::WNSNotification.new(device_urls, { type: :toast, title: 'This is a special title', message: 'this is a special message'})
16
+ # notification = RubyPushNotifications::WNS::WNSNotification.new(device_urls, { type: :tile, image: 'https://res-5.cloudinary.com/prepathon/image/fetch/w_21,h_21,c_fill,g_face/http://assets-cdn.github.com/images/icons/emoji/unicode/1f604.png?v6', message: 'this is a special message'})
17
+ # notification = RubyPushNotifications::WNS::WNSNotification.new(device_urls, { type: :badge, value: 'activity'})
18
+
19
+ pusher = RubyPushNotifications::WNS::WNSPusher.new(access_token)
20
+
21
+ pusher.push([notification])
22
+ p 'Notification sending results:'
23
+ p "Success: #{notification.success}, Failed: #{notification.failed}"
24
+ p 'Details:'
25
+ p notification.individual_results
@@ -3,3 +3,4 @@ require 'ruby-push-notifications/notification_results_manager'
3
3
  require 'ruby-push-notifications/apns'
4
4
  require 'ruby-push-notifications/gcm'
5
5
  require 'ruby-push-notifications/mpns'
6
+ require 'ruby-push-notifications/wns'
@@ -0,0 +1,6 @@
1
+ require 'ruby-push-notifications/wns/wns_access'
2
+ require 'ruby-push-notifications/wns/wns_connection'
3
+ require 'ruby-push-notifications/wns/wns_notification'
4
+ require 'ruby-push-notifications/wns/wns_pusher'
5
+ require 'ruby-push-notifications/wns/wns_result'
6
+ require 'ruby-push-notifications/wns/wns_response'
@@ -0,0 +1,82 @@
1
+ require 'uri'
2
+ require 'net/https'
3
+ require 'json'
4
+ require 'ostruct'
5
+
6
+ module RubyPushNotifications
7
+ module WNS
8
+ # This class is responsible for get access auth token for sending pushes
9
+ #
10
+ class WNSAccess
11
+
12
+ # This class is responsible for structurize response from login WNS service
13
+ #
14
+ class Response
15
+ # @return [OpenStruct]. Return structurized response
16
+ attr_reader :response
17
+
18
+ def initialize(response)
19
+ @response = structurize(response)
20
+ end
21
+
22
+ private
23
+
24
+ def structurize(response)
25
+ body = response.body.to_s.empty? ? {} : JSON.parse(response.body)
26
+ OpenStruct.new(
27
+ status_code: response.code.to_i,
28
+ status: response.message,
29
+ error: body['error'],
30
+ error_description: body['error_description'],
31
+ access_token: body['access_token'],
32
+ token_ttl: body['expires_in']
33
+ )
34
+ end
35
+ end
36
+
37
+ # @private Grant type for getting access token
38
+ GRANT_TYPE = 'client_credentials'.freeze
39
+
40
+ # @private Scope for getting access token
41
+ SCOPE = 'notify.windows.com'.freeze
42
+
43
+ # @private Url for getting access token
44
+ ACCESS_TOKEN_URL = 'https://login.live.com/accesstoken.srf'.freeze
45
+
46
+ # @return [String]. Sid
47
+ attr_reader :sid
48
+
49
+ # @return [String]. Secret token
50
+ attr_reader :secret
51
+
52
+ # @param type [String]. Sid
53
+ # @param type [String]. Secret
54
+ #
55
+ # You can get it on https://account.live.com/developers/applications/index
56
+ def initialize(sid, secret)
57
+ @sid = sid
58
+ @secret = secret
59
+ end
60
+
61
+ # Get access auth token for sending pushes
62
+ #
63
+ # https://docs.microsoft.com/en-us/windows/uwp/controls-and-patterns/tiles-and-notifications-windows-push-notification-services--wns--overview
64
+ def get_token
65
+ body = {
66
+ grant_type: GRANT_TYPE,
67
+ client_id: sid,
68
+ client_secret: secret,
69
+ scope: SCOPE
70
+ }
71
+
72
+ url = URI.parse ACCESS_TOKEN_URL
73
+ http = Net::HTTP.new url.host, url.port
74
+ http.use_ssl = true
75
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
76
+ response = http.post url.request_uri, URI.encode_www_form(body)
77
+
78
+ Response.new response
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,97 @@
1
+ require 'uri'
2
+ require 'net/https'
3
+
4
+ module RubyPushNotifications
5
+ module WNS
6
+ # Encapsulates a connection to the WNS service
7
+ # Responsible for final connection with the service.
8
+ #
9
+ class WNSConnection
10
+ # @private Content-Type HTTP Header type string
11
+ CONTENT_TYPE_HEADER = 'Content-Type'.freeze
12
+
13
+ # @private Content-Length HTTP Header type string
14
+ CONTENT_LENGTH_HEADER = 'Content-Length'.freeze
15
+
16
+ # @private WNS type string
17
+ X_WNS_TYPE_HEADER = 'X-WNS-Type'.freeze
18
+
19
+ # @private Authorization string
20
+ AUTHORIZATION_HEADER = 'Authorization'.freeze
21
+
22
+ # @private Request for status type boolean
23
+ REQUEST_FOR_STATUS_HEADER = 'X-WNS-RequestForStatus'.freeze
24
+
25
+ # @private Content-Type type string
26
+ CONTENT_TYPE = {
27
+ badge: 'text/xml',
28
+ toast: 'text/xml',
29
+ tile: 'text/xml',
30
+ raw: 'application/octet-stream'
31
+ }.freeze
32
+
33
+ # @private Windows Phone Target Types
34
+ WP_TARGETS = {
35
+ badge: 'wns/badge',
36
+ toast: 'wns/toast',
37
+ tile: 'wns/tile',
38
+ raw: 'wns/raw'
39
+ }.freeze
40
+
41
+ # Issues a POST request to the WNS send endpoint to
42
+ # submit the given notifications.
43
+ #
44
+ # @param notifications [WNSNotification]. The notifications object to POST
45
+ # @param access_token [String] required. Access token for send push
46
+ # @param options [Hash] optional. Options for GCMPusher. Currently supports:
47
+ # * open_timeout [Integer]: Number of seconds to wait for the connection to open. Defaults to 30.
48
+ # * read_timeout [Integer]: Number of seconds to wait for one block to be read. Defaults to 30.
49
+ # @return [Array]. The response of post
50
+ # (http://msdn.microsoft.com/pt-br/library/windows/apps/ff941099)
51
+ def self.post(notifications, access_token, options = {})
52
+ body = notifications.as_wns_xml
53
+ headers = build_headers(access_token, notifications.data[:type], body.length.to_s)
54
+ responses = []
55
+ notifications.each_device do |url|
56
+ http = Net::HTTP.new url.host, url.port
57
+ http.open_timeout = options.fetch(:open_timeout, 30)
58
+ http.read_timeout = options.fetch(:read_timeout, 30)
59
+ if url.scheme == 'https'
60
+ http.use_ssl = true
61
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
62
+ end
63
+ response = http.post url.request_uri, body, headers
64
+ responses << { device_url: url.to_s, headers: capitalize_headers(response), code: response.code.to_i }
65
+ end
66
+ WNSResponse.new responses
67
+ end
68
+
69
+ # Build Header based on type and delay
70
+ #
71
+ # @param type [String]. Access token
72
+ # @param type [Symbol]. The type of notification
73
+ # @param type [String]. Content length of body
74
+ # @return [Hash]. Correct delay based on notification type
75
+ # https://msdn.microsoft.com/en-us/library/windows/apps/hh465435.aspx#send_notification_request
76
+ def self.build_headers(access_token, type, body_length)
77
+ {
78
+ CONTENT_TYPE_HEADER => CONTENT_TYPE[type],
79
+ X_WNS_TYPE_HEADER => WP_TARGETS[type],
80
+ CONTENT_LENGTH_HEADER => body_length,
81
+ AUTHORIZATION_HEADER => "Bearer #{access_token}",
82
+ REQUEST_FOR_STATUS_HEADER => 'true'
83
+ }
84
+ end
85
+
86
+ # Extract headers from response
87
+ # @param response [Net::HTTPResponse]. HTTP response for request
88
+ #
89
+ # @return [Hash]. Hash with headers with case-insensitive keys and capitalized string values
90
+ def self.capitalize_headers(response)
91
+ headers = {}
92
+ response.each_header { |k, v| headers[k] = v.capitalize }
93
+ headers
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,101 @@
1
+ require 'builder'
2
+
3
+ module RubyPushNotifications
4
+ module WNS
5
+ # Encapsulates a WNS Notification.
6
+ # Actually support for raw, toast, tiles notifications
7
+ # (http://msdn.microsoft.com/en-us/library/windowsphone/develop/hh202945)
8
+ #
9
+ class WNSNotification
10
+ include RubyPushNotifications::NotificationResultsManager
11
+
12
+ # @return [Hash]. Payload to send.
13
+ # Toast :title => a bold message
14
+ # :message => the small message
15
+ # :param => a string parameter that is passed to the app
16
+ # Tile :image => a new image for the tile
17
+ # :count => a number to show on the tile
18
+ # :title => the new title of the tile
19
+ # :back_image => an image for the back of the tile
20
+ # :back_title => a title on the back of the tile
21
+ # :back_content => some content (text) for the back
22
+ # Raw :message => the full Hash message body
23
+ attr_reader :data
24
+
25
+ # @return [Array]. Array with the receiver's WNS device URLs.
26
+ attr_reader :device_urls
27
+
28
+ # Initializes the notification
29
+ #
30
+ # @param [Array]. Array with the receiver's device urls.
31
+ # @param [Hash]. Payload to send.
32
+ # Toast :title => a bold message
33
+ # :message => the small message
34
+ # :param => a string parameter that is passed to the app
35
+ # Tile :image => a new image for the tile
36
+ # :count => a number to show on the tile
37
+ # :title => the new title of the tile
38
+ # :back_image => an image for the back of the tile
39
+ # :back_title => a title on the back of the tile
40
+ # :back_content => some content (text) for the back
41
+ # Raw :message => the full XML message body
42
+ def initialize(device_urls, data)
43
+ @device_urls = device_urls
44
+ @data = data
45
+ @data[:type] ||= :raw
46
+ end
47
+
48
+ # @return [String]. The WNS's XML format for the payload to send.
49
+ # (https://docs.microsoft.com/en-us/uwp/schemas/tiles/tiles-xml-schema-portal)
50
+ def as_wns_xml
51
+ xml = Builder::XmlMarkup.new
52
+ xml.instruct!
53
+ if data[:type] != :raw
54
+ case data[:type]
55
+ when :toast
56
+ xml.tag!('toast', **launch_params(data)) do
57
+ xml.tag!('visual') do
58
+ xml.tag!('binding', template: data[:template] || 'ToastText02') do
59
+ xml.tag!('text', id: 1) { xml.text!(data[:title].to_s) }
60
+ xml.tag!('text', id: 2) { xml.text!(data[:message].to_s) }
61
+ end
62
+ end
63
+ end
64
+ when :tile
65
+ xml.tag!('tile') do
66
+ xml.tag!('visual') do
67
+ xml.tag!('binding', template: data[:template] || 'TileWideImageAndText01') do
68
+ xml.tag!('image', src: data[:image].to_s)
69
+ xml.tag!('text') { xml.text!(data[:message].to_s) }
70
+ end
71
+ end
72
+ end
73
+ when :badge
74
+ xml.tag!('badge', value: data[:value])
75
+ end
76
+ else
77
+ xml.root { build_hash(xml, data[:message]) }
78
+ end
79
+ xml.target!
80
+ end
81
+
82
+ def each_device
83
+ @device_urls.each do |url|
84
+ yield(URI.parse url)
85
+ end
86
+ end
87
+
88
+ def build_hash(xml, options)
89
+ return unless options
90
+ options.each do |k, v|
91
+ xml.tag!(k.to_s) { v.is_a?(Hash) ? build_hash(xml, v) : xml.text!(v.to_s) }
92
+ end
93
+ end
94
+
95
+ def launch_params(data)
96
+ return {} unless data[:param]
97
+ { launch: data[:param].to_json }
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,32 @@
1
+ module RubyPushNotifications
2
+ module WNS
3
+
4
+ # This class is responsible for sending notifications to the WNS service.
5
+ #
6
+ class WNSPusher
7
+
8
+ # Initializes the WNSPusher
9
+ #
10
+ # @param access_token [String]. WNS access token.
11
+ # @param options [Hash] optional. Options for GCMPusher. Currently supports:
12
+ # * open_timeout [Integer]: Number of seconds to wait for the connection to open. Defaults to 30.
13
+ # * read_timeout [Integer]: Number of seconds to wait for one block to be read. Defaults to 30.
14
+ # (http://msdn.microsoft.com/pt-br/library/windows/apps/ff941099)
15
+ def initialize(access_token, options = {})
16
+ @access_token = access_token
17
+ @options = options
18
+ end
19
+
20
+ # Actually pushes the given notifications.
21
+ # Assigns every notification an array with the result of each
22
+ # individual notification.
23
+ #
24
+ # @param notifications [Array]. Array of WNSNotification to send.
25
+ def push(notifications)
26
+ notifications.each do |notif|
27
+ notif.results = WNSConnection.post notif, @access_token, @options
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,83 @@
1
+ module RubyPushNotifications
2
+ module WNS
3
+
4
+ # This class encapsulates a response received from the WNS service
5
+ # and helps parsing and understanding the received messages/codes.
6
+ #
7
+ class WNSResponse
8
+
9
+ # @return [Integer] the number of successfully sent notifications
10
+ attr_reader :success
11
+
12
+ # @return [Integer] the number of failed notifications
13
+ attr_reader :failed
14
+
15
+ # @return [Array] Array of a WNSResult for every receiver of the notification
16
+ # sent indicating the result of the operation.
17
+ attr_reader :results
18
+ alias_method :individual_results, :results
19
+
20
+ # Initializes the WNSResponse and runs response parsing
21
+ #
22
+ # @param responses [Array]. Array with device_urls and http responses
23
+ def initialize(responses)
24
+ parse_response responses
25
+ end
26
+
27
+ def ==(other)
28
+ (other.is_a?(WNSResponse) &&
29
+ success == other.success &&
30
+ failed == other.failed &&
31
+ results == other.results) || super(other)
32
+ end
33
+
34
+ private
35
+
36
+ # Parses the response extracting counts for successful, failed messages.
37
+ # Also creates the results array assigning a WNSResult subclass for each
38
+ # device URL the notification was sent to.
39
+ #
40
+ # @param responses [Array]. Array of hash responses
41
+ def parse_response(responses)
42
+ @success = responses.count { |response| response[:code] == 200 }
43
+ @failed = responses.count { |response| response[:code] != 200 }
44
+ @results = responses.map do |response|
45
+ wns_result_for response[:code],
46
+ response[:device_url],
47
+ response[:headers]
48
+ end
49
+ end
50
+
51
+ # Factory method that, for each WNS result object assigns a WNSResult
52
+ # subclass.
53
+ #
54
+ # @param code [Integer]. The HTTP status code received
55
+ # @param device_url [String]. The receiver's WNS device url.
56
+ # @param headers [Hash]. The HTTP headers received.
57
+ # @return [WNSResult]. Corresponding WNSResult subclass
58
+ def wns_result_for(code, device_url, headers)
59
+ case code
60
+ when 200
61
+ WNSResultOK.new device_url, headers
62
+ when 400
63
+ MalformedWNSResultError.new device_url
64
+ when 401
65
+ WNSAuthError.new device_url
66
+ when 404
67
+ WNSInvalidError.new device_url, headers
68
+ when 406
69
+ WNSLimitError.new device_url, headers
70
+ when 410
71
+ WNSExpiredError.new device_url, headers
72
+ when 412
73
+ WNSPreConditionError.new device_url, headers
74
+ when 500..599
75
+ WNSInternalError.new device_url
76
+ else
77
+ WNSResultError.new device_url
78
+ end
79
+ end
80
+
81
+ end
82
+ end
83
+ end