Skip to content

Commit 839b8fc

Browse files
authored
Merge pull request #101 from pusher/native-notifications
Native notifications
2 parents 4776a6d + cab80ac commit 839b8fc

File tree

6 files changed

+421
-7
lines changed

6 files changed

+421
-7
lines changed

README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,3 +247,63 @@ else
247247
render text: 'invalid', status: 401
248248
end
249249
```
250+
251+
## Push Notifications (BETA)
252+
253+
Pusher now allows sending native notifications to iOS and Android devices. Check out the [documentation](https://pusher.com/docs/push_notifications) for information on how to set up push notifications on Android and iOS. There is no additional setup required to use it with this library. It works out of the box wit the same Pusher instance. All you need are the same pusher credentials.
254+
255+
### Sending native pushes
256+
257+
The native notifications API is hosted at `nativepush-cluster1.pusher.com` and only accepts https requests.
258+
259+
You can send pushes by using the `notify` method, either globally or on the instance. The method takes two parameters:
260+
261+
- `interests`: An Array of strings which represents the interests your devices are subscribed to. These are akin to channels in the DDN with less of an epehemeral nature. Note that currently, you can only send to _one_ interest.
262+
- `data`: The content of the notification represented by a Hash. You must supply either the `gcm` or `apns` key. For a detailed list of the acceptable keys, take a look at the [iOS](https://pusher.com/docs/push_notifications/ios/server) and [Android](https://pusher.com/docs/push_notifications/android/server) docs.
263+
264+
Example:
265+
266+
```ruby
267+
data = {
268+
apns: {
269+
aps: {
270+
alert: {
271+
body: 'tada'
272+
}
273+
}
274+
}
275+
}
276+
277+
pusher.notify(["my-favourite-interest"], data)
278+
```
279+
280+
### Errors
281+
282+
Push notification requests, once submitted to the service are executed asynchronously. To make reporting errors easier, you can supply a `webhook_url` field in the body of the request. This will be used by the service to send a webhook to the supplied URL if there are errors.
283+
284+
You may also supply a `webhook_level` field in the body, which can either be INFO or DEBUG. It defaults to INFO - where INFO only reports customer facing errors, while DEBUG reports all errors.
285+
286+
For example:
287+
288+
```ruby
289+
data = {
290+
apns: {
291+
aps: {
292+
alert: {
293+
body: "hello"
294+
}
295+
}
296+
},
297+
gcm: {
298+
notification: {
299+
title: "hello",
300+
icon: "icon"
301+
}
302+
},
303+
webhook_url: "http://yolo.com",
304+
webhook_level: "INFO"
305+
}
306+
```
307+
308+
**NOTE:** This is currently a BETA feature and there might be minor bugs and issues. Changes to the API will be kept to a minimum, but changes are expected. If you come across any bugs or issues, please do get in touch via [support](support@pusher.com) or create an issue here.
309+

lib/pusher.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ class << self
2828
extend Forwardable
2929

3030
def_delegators :default_client, :scheme, :host, :port, :app_id, :key, :secret, :http_proxy
31+
def_delegators :default_client, :notification_host, :notification_scheme
3132
def_delegators :default_client, :scheme=, :host=, :port=, :app_id=, :key=, :secret=, :http_proxy=
33+
def_delegators :default_client, :notification_host=, :notification_scheme=
3234

3335
def_delegators :default_client, :authentication_token, :url
3436
def_delegators :default_client, :encrypted=, :url=, :cluster=
@@ -37,6 +39,7 @@ class << self
3739
def_delegators :default_client, :get, :get_async, :post, :post_async
3840
def_delegators :default_client, :channels, :channel_info, :channel_users, :trigger, :trigger_async
3941
def_delegators :default_client, :authenticate, :webhook, :channel, :[]
42+
def_delegators :default_client, :notify
4043

4144
attr_writer :logger
4245

@@ -61,3 +64,4 @@ def default_client
6164
require 'pusher/request'
6265
require 'pusher/resource'
6366
require 'pusher/webhook'
67+
require 'pusher/native_notification/client'

lib/pusher/client.rb

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
module Pusher
44
class Client
5-
attr_accessor :scheme, :host, :port, :app_id, :key, :secret
5+
attr_accessor :scheme, :host, :port, :app_id, :key, :secret, :notification_host, :notification_scheme
66
attr_reader :http_proxy, :proxy
77
attr_writer :connect_timeout, :send_timeout, :receive_timeout,
88
:keep_alive_timeout
@@ -32,9 +32,17 @@ def initialize(options = {})
3232
merged_options[:host] = "api.pusherapp.com"
3333
end
3434

35-
@scheme, @host, @port, @app_id, @key, @secret = merged_options.values_at(
36-
:scheme, :host, :port, :app_id, :key, :secret
37-
)
35+
# TODO: Change host name when finalized
36+
merged_options[:notification_host] =
37+
options.fetch(:notification_host, "nativepush-cluster1.pusher.com")
38+
39+
merged_options[:notification_scheme] =
40+
options.fetch(:notification_scheme, "https")
41+
42+
@scheme, @host, @port, @app_id, @key, @secret, @notification_host, @notification_scheme =
43+
merged_options.values_at(
44+
:scheme, :host, :port, :app_id, :key, :secret, :notification_host, :notification_scheme
45+
)
3846

3947
@http_proxy = nil
4048
self.http_proxy = options[:http_proxy] if options[:http_proxy]
@@ -298,6 +306,25 @@ def trigger_batch_async(*events)
298306
post_async('/batch_events', trigger_batch_params(events.flatten))
299307
end
300308

309+
def notification_client
310+
@notification_client ||=
311+
NativeNotification::Client.new(@app_id, @notification_host, @notification_scheme, self)
312+
end
313+
314+
315+
# Send a push notification
316+
#
317+
# POST /apps/[app_id]/notifications
318+
#
319+
# @param interests [Array] An array of interests
320+
# @param message [String] Message to send
321+
# @param options [Hash] Additional platform specific options
322+
#
323+
# @return [Hash]
324+
def notify(interests, data = {})
325+
notification_client.notify(interests, data)
326+
end
327+
301328
# Generate the expected response for an authentication endpoint.
302329
# See http://pusher.com/docs/authenticating_users for details.
303330
#
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
module Pusher
2+
module NativeNotification
3+
class Client
4+
attr_reader :app_id, :host
5+
6+
API_PREFIX = "customer_api"
7+
API_VERSION = "v1"
8+
GCM_TTL = 241920
9+
RESTRICTED_GCM_PAYLOAD_KEYS = [:to, :registration_ids]
10+
WEBHOOK_LEVELS = ["DEBUG", "INFO"]
11+
12+
def initialize(app_id, host, scheme, pusher_client)
13+
@app_id = app_id
14+
@host = host
15+
@scheme = scheme
16+
@pusher_client = pusher_client
17+
end
18+
19+
# Send a notification via the native notifications API
20+
def notify(interests, data = {})
21+
Request.new(
22+
@pusher_client,
23+
:post,
24+
url("/notifications"),
25+
{},
26+
payload(interests, data)
27+
).send_sync
28+
end
29+
30+
private
31+
32+
# TODO: Actual links
33+
#
34+
# {
35+
# interests: [Array of interests],
36+
# apns: {
37+
# See https://pusher.com/docs/push_notifications/ios/server
38+
# },
39+
# gcm: {
40+
# See https://pusher.com/docs/push_notifications/android/server
41+
# }
42+
# }
43+
#
44+
# @raise [Pusher::Error] if the `apns` or `gcm` key does not exist
45+
# @return [String]
46+
def payload(interests, data)
47+
interests = Array(interests).map(&:to_s)
48+
49+
raise Pusher::Error, "Too many interests provided" if interests.length > 1
50+
51+
data = deep_symbolize_keys!(data)
52+
validate_payload(data)
53+
54+
data.merge!(interests: interests)
55+
56+
MultiJson.encode(data)
57+
end
58+
59+
def url(path = nil)
60+
URI.parse("#{@scheme}://#{@host}/#{API_PREFIX}/#{API_VERSION}/apps/#{@app_id}#{path}")
61+
end
62+
63+
# Validate payload
64+
# `time_to_live` -> value b/w 0 and 241920
65+
# If the `notification` key is provided, ensure
66+
# that there is an accompanying `title` and `icon`
67+
# field
68+
def validate_payload(payload)
69+
unless (payload.has_key?(:apns) || payload.has_key?(:gcm))
70+
raise Pusher::Error, "GCM or APNS data must be provided"
71+
end
72+
73+
if (gcm_payload = payload[:gcm])
74+
# Restricted keys
75+
RESTRICTED_GCM_PAYLOAD_KEYS.each { |k| gcm_payload.delete(k) }
76+
if (ttl = gcm_payload[:time_to_live])
77+
78+
if ttl.to_i < 0 || ttl.to_i > GCM_TTL
79+
raise Pusher::Error, "Time to live must be between 0 and 241920 (4 weeks)"
80+
end
81+
end
82+
83+
# If the notification key is provided
84+
# validate the `icon` and `title`keys
85+
if (notification = gcm_payload[:notification])
86+
notification_title, notification_icon = notification.values_at(:title, :icon)
87+
88+
if (!notification_title || notification_title.empty?)
89+
raise Pusher::Error, "Notification title is a required field"
90+
end
91+
92+
if (!notification_icon || notification_icon.empty?)
93+
raise Pusher::Error, "Notification icon is a required field"
94+
end
95+
end
96+
end
97+
98+
if (webhook_url = payload[:webhook_url])
99+
raise Pusher::Error, "Webhook url is invalid" unless webhook_url =~ /\A#{URI::regexp(['http', 'https'])}\z/
100+
end
101+
102+
if (webhook_level = payload[:webhook_level])
103+
raise Pusher::Error, "Webhook level cannot be used without a webhook url" if !payload.has_key?(:webhook_url)
104+
105+
unless WEBHOOK_LEVELS.include?(webhook_level.upcase)
106+
raise Pusher::Error, "Webhook level must either be INFO or DEBUG"
107+
end
108+
end
109+
end
110+
111+
# Symbolize all keys in the hash recursively
112+
def deep_symbolize_keys!(hash)
113+
hash.keys.each do |k|
114+
ks = k.respond_to?(:to_sym) ? k.to_sym : k
115+
hash[ks] = hash.delete(k)
116+
deep_symbolize_keys!(hash[ks]) if hash[ks].kind_of?(Hash)
117+
end
118+
119+
hash
120+
end
121+
end
122+
end
123+
end

pusher.gemspec

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ Gem::Specification.new do |s|
1818
s.add_development_dependency "rspec", "~> 3.0"
1919
s.add_development_dependency "webmock"
2020
s.add_development_dependency "em-http-request", "~> 1.1.0"
21-
s.add_development_dependency "rake"
22-
s.add_development_dependency "rack"
23-
s.add_development_dependency "json"
21+
s.add_development_dependency "rake", "~> 10.4.2"
22+
s.add_development_dependency "rack", "~> 1.6.4"
23+
s.add_development_dependency "json", "~> 1.8.3"
2424

2525
s.files = `git ls-files`.split("\n")
2626
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")

0 commit comments

Comments
 (0)