ruby-push-notifications 1.1.0 → 1.2.0

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