apn_sender 2.0.2 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/apn.rb +3 -2
- data/lib/apn/application.rb +37 -0
- data/lib/apn/jobs/sidekiq_notification_job.rb +2 -2
- data/lib/apn/multiple_apps.rb +53 -0
- data/lib/apn/notification.rb +25 -45
- data/lib/apn/payload.rb +70 -0
- data/lib/apn/version.rb +1 -1
- metadata +31 -22
- data/CHANGELOG.md +0 -28
- data/LICENSE +0 -20
- data/README.md +0 -151
- data/Rakefile +0 -35
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6a84e566b3256a5f25795f6264af593041e7c800
|
4
|
+
data.tar.gz: cbe6ffd3cc326f919f702c883c1c608d9ed58535
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8dc1ed6e420f195f9154c145b3a5c3ca53d0de4b76c4b55b64b8c77dcfdeee2dded1d2279d9c4cc643650681cba1225744550a3314cd8cb28ce6197b47eaab1b
|
7
|
+
data.tar.gz: 0782593c57733813f5ad1a9b6b6a50f50d4550550e0583ce2a4168b505a0d1bdd47fbfde2cd217b0dbcf16f66f8b7b8e8cf84d02c2fb2f3e6251db08e28e521e
|
data/lib/apn.rb
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
require "openssl"
|
2
2
|
require "socket"
|
3
|
+
require "active_support"
|
3
4
|
require "active_support/core_ext"
|
4
5
|
require "active_support/json"
|
5
6
|
require 'connection_pool'
|
6
7
|
|
7
8
|
require "apn/version"
|
8
|
-
require
|
9
|
+
require "apn/connection"
|
9
10
|
|
10
11
|
module APN
|
11
|
-
|
12
12
|
class << self
|
13
13
|
include APN::Connection
|
14
14
|
|
@@ -83,6 +83,7 @@ module APN
|
|
83
83
|
end
|
84
84
|
end
|
85
85
|
|
86
|
+
require 'apn/multiple_apps' if ENV['APN_MULTIPLE_APPS'] == 'true'
|
86
87
|
require 'apn/notification'
|
87
88
|
require 'apn/client'
|
88
89
|
require 'apn/feedback'
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "apn/connection"
|
2
|
+
|
3
|
+
module APN
|
4
|
+
class Application
|
5
|
+
include Connection
|
6
|
+
|
7
|
+
APPS = {}
|
8
|
+
OPTION_KEYS = [:pool_size, :pool_timeout, :host, :port, :root, :full_certificate_path, :password, :certificate_name].freeze
|
9
|
+
DELEGATE_METHODS = [:with_connection, :connection_pool].concat(OPTION_KEYS)
|
10
|
+
|
11
|
+
attr_reader :name
|
12
|
+
|
13
|
+
def initialize(name, options = {})
|
14
|
+
@name = name.to_s
|
15
|
+
|
16
|
+
OPTION_KEYS.each do |key|
|
17
|
+
self.send("#{key}=", options.fetch(key) { APN.send("original_#{key}") } )
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_h
|
22
|
+
Hash[OPTION_KEYS.zip(OPTION_KEYS.map(&method(:send)))]
|
23
|
+
end
|
24
|
+
|
25
|
+
def == other
|
26
|
+
if other.is_a?(APN::Application)
|
27
|
+
to_h == other.to_h
|
28
|
+
else
|
29
|
+
super(other)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.register(*args)
|
34
|
+
new(*args).tap { |app| APPS[app.name] = app if app.certificate }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -1,11 +1,11 @@
|
|
1
1
|
module APN::Jobs
|
2
2
|
# This is the class that's actually enqueued via Sidekiq when user calls +APN.notify+.
|
3
|
-
# It gets added to the +
|
3
|
+
# It gets added to the +apple_push_notifications+ Sidekiq queue, which should only be operated on by
|
4
4
|
# workers of the +APN::Sender+ class.
|
5
5
|
class SidekiqNotificationJob
|
6
6
|
include Sidekiq::Worker
|
7
7
|
# Behind the scenes, this is the name of our Sidekiq queue
|
8
|
-
|
8
|
+
sidekiq_options :queue => QUEUE_NAME
|
9
9
|
|
10
10
|
# Build a notification from arguments and send to Apple
|
11
11
|
def perform(token, opts)
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require "apn/application"
|
2
|
+
|
3
|
+
module APN
|
4
|
+
module MultipleApps
|
5
|
+
def self.extended(mod)
|
6
|
+
class << mod
|
7
|
+
alias_method_chain :notify_sync, :app
|
8
|
+
|
9
|
+
delegate(*Application::DELEGATE_METHODS, to: :current_app, prefix: true, allow_nil: true)
|
10
|
+
|
11
|
+
Application::DELEGATE_METHODS.each do |method_name|
|
12
|
+
alias_method :"original_#{method_name}", method_name
|
13
|
+
alias_method method_name, :"current_app_#{method_name}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def notify_sync_with_app(token, notification)
|
19
|
+
if notification.is_a?(Hash)
|
20
|
+
notification.symbolize_keys!
|
21
|
+
app_name = notification.delete(:app)
|
22
|
+
end
|
23
|
+
|
24
|
+
with_app(app_name) do
|
25
|
+
notify_sync_without_app(token, notification)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_writer :default_app_name
|
30
|
+
|
31
|
+
def default_app_name
|
32
|
+
@default_app_name || 'default'.freeze
|
33
|
+
end
|
34
|
+
|
35
|
+
def current_app_name
|
36
|
+
@_app_name || default_app_name
|
37
|
+
end
|
38
|
+
|
39
|
+
def current_app
|
40
|
+
Application::APPS[current_app_name] or \
|
41
|
+
raise NameError, "Unregistered APN::Application `#{current_app_name}'"
|
42
|
+
end
|
43
|
+
|
44
|
+
def with_app(app_name)
|
45
|
+
@_app_name, app_was = app_name.presence, @_app_name
|
46
|
+
yield if block_given?
|
47
|
+
ensure
|
48
|
+
@_app_name = app_was
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
APN.extend APN::MultipleApps
|
data/lib/apn/notification.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'apn/payload'
|
2
|
+
|
1
3
|
module APN
|
2
4
|
# Encapsulates the logic necessary to convert an iPhone token and an array of options into a string of the format required
|
3
5
|
# by Apple's servers to send the notification. Much of the processing code here copied with many thanks from
|
@@ -19,9 +21,9 @@ module APN
|
|
19
21
|
#
|
20
22
|
class Notification
|
21
23
|
# Available to help clients determine before they create the notification if their message will be too large.
|
22
|
-
# Each iPhone Notification payload must be
|
23
|
-
# https://developer.apple.com/library/
|
24
|
-
DATA_MAX_BYTES =
|
24
|
+
# Each iPhone Notification payload must be 2047 or fewer characters (not including the token or other push data), see Apple specs at:
|
25
|
+
# https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW1
|
26
|
+
DATA_MAX_BYTES = 2047
|
25
27
|
|
26
28
|
attr_accessor :options, :token
|
27
29
|
def initialize(token, opts)
|
@@ -47,9 +49,23 @@ module APN
|
|
47
49
|
|
48
50
|
# Completed encoded notification, ready to send down the wire to Apple
|
49
51
|
def packaged_notification
|
50
|
-
|
51
|
-
|
52
|
-
|
52
|
+
[2, frame_length, packaged_frame].pack("cl>a*")
|
53
|
+
end
|
54
|
+
|
55
|
+
def frame_length
|
56
|
+
packaged_frame.bytesize
|
57
|
+
end
|
58
|
+
|
59
|
+
def packaged_frame
|
60
|
+
[token_frame, payload_frame].pack('a*a*')
|
61
|
+
end
|
62
|
+
|
63
|
+
def token_frame
|
64
|
+
[1, 32, packaged_token].pack('cs>a*')
|
65
|
+
end
|
66
|
+
|
67
|
+
def payload_frame
|
68
|
+
[2, payload_size, packaged_message].pack('cs>a*')
|
53
69
|
end
|
54
70
|
|
55
71
|
# Device token, compressed and hex-ified
|
@@ -58,7 +74,7 @@ module APN
|
|
58
74
|
end
|
59
75
|
|
60
76
|
# Converts the supplied options into the JSON needed for Apple's push notification servers.
|
61
|
-
# Extracts :alert, :badge, and :
|
77
|
+
# Extracts :alert, :badge, :sound and :category keys into the 'aps' hash, merges any other hash data
|
62
78
|
# into the root of the hash to encode and send to apple.
|
63
79
|
def packaged_message
|
64
80
|
@packaged_message ||=
|
@@ -70,6 +86,7 @@ module APN
|
|
70
86
|
hsh['aps']['alert'] = alert
|
71
87
|
end
|
72
88
|
hsh['aps']['badge'] = opts.delete(:badge).to_i if opts[:badge]
|
89
|
+
hsh['aps']['category'] = opts.delete(:category).to_s if opts[:category]
|
73
90
|
if sound = opts.delete(:sound)
|
74
91
|
hsh['aps']['sound'] = sound.is_a?(TrueClass) ? 'default' : sound.to_s
|
75
92
|
end
|
@@ -77,45 +94,8 @@ module APN
|
|
77
94
|
hsh['aps']['content-available'] = 1 if [1,true].include? content_available
|
78
95
|
end
|
79
96
|
hsh.merge!(opts)
|
80
|
-
|
97
|
+
Payload.new(hsh, DATA_MAX_BYTES).package
|
81
98
|
end
|
82
99
|
end
|
83
|
-
|
84
|
-
private
|
85
|
-
|
86
|
-
def payload(hash)
|
87
|
-
str = ActiveSupport::JSON::encode(hash)
|
88
|
-
|
89
|
-
if APN.truncate_alert && str.bytesize > DATA_MAX_BYTES
|
90
|
-
if hash['aps']['alert'].is_a?(Hash)
|
91
|
-
alert = hash['aps']['alert']['loc-args'][0]
|
92
|
-
else
|
93
|
-
alert = hash['aps']['alert']
|
94
|
-
end
|
95
|
-
max_bytesize = DATA_MAX_BYTES - (str.bytesize - alert.bytesize)
|
96
|
-
|
97
|
-
raise "Even truncating the alert wont be enought to have a #{DATA_MAX_BYTES} message" if max_bytesize <= 0
|
98
|
-
alert = truncate_alert(alert, max_bytesize)
|
99
|
-
|
100
|
-
if hash['aps']['alert'].is_a?(Hash)
|
101
|
-
hash['aps']['alert']['loc-args'][0] = alert
|
102
|
-
else
|
103
|
-
hash['aps']['alert'] = alert
|
104
|
-
end
|
105
|
-
str = ActiveSupport::JSON::encode(hash)
|
106
|
-
end
|
107
|
-
str
|
108
|
-
end
|
109
|
-
|
110
|
-
def truncate_alert(alert, max_size)
|
111
|
-
alert.each_char.each_with_object('') do |char, result|
|
112
|
-
if result.bytesize + char.bytesize > max_size
|
113
|
-
break result
|
114
|
-
else
|
115
|
-
result << char
|
116
|
-
end
|
117
|
-
end
|
118
|
-
end
|
119
|
-
|
120
100
|
end
|
121
101
|
end
|
data/lib/apn/payload.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
class Payload
|
2
|
+
|
3
|
+
def initialize(notification_hash, max_bytes)
|
4
|
+
@notification_hash = notification_hash
|
5
|
+
@max_bytes = max_bytes
|
6
|
+
end
|
7
|
+
|
8
|
+
def package
|
9
|
+
str = encode(@notification_hash)
|
10
|
+
|
11
|
+
if APN.truncate_alert && str.bytesize > @max_bytes
|
12
|
+
max_bytesize = @max_bytes - (str.bytesize - alert.bytesize)
|
13
|
+
|
14
|
+
if max_bytesize <= 0
|
15
|
+
escaped_max_bytesize = @max_bytes - (str.bytesize - encode(alert).bytesize)
|
16
|
+
raise "Even truncating the alert won't be enough to have a #{@max_bytes} message" if escaped_max_bytesize <= 0
|
17
|
+
truncate_escaped!(escaped_max_bytesize)
|
18
|
+
else
|
19
|
+
truncate_alert!(max_bytesize)
|
20
|
+
end
|
21
|
+
str = encode(@notification_hash)
|
22
|
+
end
|
23
|
+
str
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def alert
|
29
|
+
@alert ||=
|
30
|
+
if hash_alert?
|
31
|
+
@notification_hash['aps']['alert']['loc-args'][0]
|
32
|
+
else
|
33
|
+
@notification_hash['aps']['alert']
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def alert=(value)
|
38
|
+
if hash_alert?
|
39
|
+
@notification_hash['aps']['alert']['loc-args'][0] = value
|
40
|
+
else
|
41
|
+
@notification_hash['aps']['alert'] = value
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def hash_alert?
|
46
|
+
@hash_alert ||= @notification_hash['aps']['alert'].is_a?(Hash)
|
47
|
+
end
|
48
|
+
|
49
|
+
def truncate_alert!(max_size)
|
50
|
+
self.alert = alert.mb_chars.limit(max_size).to_s
|
51
|
+
end
|
52
|
+
|
53
|
+
def truncate_escaped!(max_size)
|
54
|
+
self.alert = alert.each_char.each_with_object('') do |char, result|
|
55
|
+
if encoded_size(result) + encoded_size(char) > max_size
|
56
|
+
break result
|
57
|
+
else
|
58
|
+
result << char
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def encode(obj)
|
64
|
+
ActiveSupport::JSON.encode(obj)
|
65
|
+
end
|
66
|
+
|
67
|
+
def encoded_size(str)
|
68
|
+
encode(str).bytesize - 2
|
69
|
+
end
|
70
|
+
end
|
data/lib/apn/version.rb
CHANGED
metadata
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: apn_sender
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0
|
5
|
-
prerelease:
|
4
|
+
version: 2.1.0
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Kali Donovan
|
@@ -14,40 +13,52 @@ date: 2011-05-15 00:00:00.000000000 Z
|
|
14
13
|
dependencies:
|
15
14
|
- !ruby/object:Gem::Dependency
|
16
15
|
name: connection_pool
|
17
|
-
requirement:
|
18
|
-
none: false
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
19
17
|
requirements:
|
20
18
|
- - ">="
|
21
19
|
- !ruby/object:Gem::Version
|
22
20
|
version: '0'
|
23
21
|
type: :runtime
|
24
22
|
prerelease: false
|
25
|
-
version_requirements:
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0'
|
26
28
|
- !ruby/object:Gem::Dependency
|
27
29
|
name: activesupport
|
28
|
-
requirement:
|
29
|
-
none: false
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
30
31
|
requirements:
|
31
32
|
- - ">="
|
32
33
|
- !ruby/object:Gem::Version
|
33
34
|
version: '3.1'
|
34
35
|
- - "<"
|
35
36
|
- !ruby/object:Gem::Version
|
36
|
-
version:
|
37
|
+
version: '5'
|
37
38
|
type: :runtime
|
38
39
|
prerelease: false
|
39
|
-
version_requirements:
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '3.1'
|
45
|
+
- - "<"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5'
|
40
48
|
- !ruby/object:Gem::Dependency
|
41
49
|
name: daemons
|
42
|
-
requirement:
|
43
|
-
none: false
|
50
|
+
requirement: !ruby/object:Gem::Requirement
|
44
51
|
requirements:
|
45
52
|
- - ">="
|
46
53
|
- !ruby/object:Gem::Version
|
47
54
|
version: '0'
|
48
55
|
type: :runtime
|
49
56
|
prerelease: false
|
50
|
-
version_requirements:
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
51
62
|
description: Background worker to send Apple Push Notifications over a persistent
|
52
63
|
TCP socket. Includes Resque tweaks to allow persistent sockets between jobs, helper
|
53
64
|
methods for enqueueing APN notifications, and a background daemon to send them.
|
@@ -56,49 +67,47 @@ executables: []
|
|
56
67
|
extensions: []
|
57
68
|
extra_rdoc_files: []
|
58
69
|
files:
|
70
|
+
- lib/apn.rb
|
71
|
+
- lib/apn/application.rb
|
72
|
+
- lib/apn/backend.rb
|
59
73
|
- lib/apn/backend/resque.rb
|
60
74
|
- lib/apn/backend/sidekiq.rb
|
61
|
-
- lib/apn/backend.rb
|
62
75
|
- lib/apn/client.rb
|
63
76
|
- lib/apn/connection.rb
|
64
77
|
- lib/apn/feedback.rb
|
65
78
|
- lib/apn/jobs/resque_notification_job.rb
|
66
79
|
- lib/apn/jobs/sidekiq_notification_job.rb
|
80
|
+
- lib/apn/multiple_apps.rb
|
67
81
|
- lib/apn/notification.rb
|
82
|
+
- lib/apn/payload.rb
|
68
83
|
- lib/apn/railtie.rb
|
69
84
|
- lib/apn/sender_daemon.rb
|
70
85
|
- lib/apn/tasks.rb
|
71
86
|
- lib/apn/version.rb
|
72
|
-
- lib/apn.rb
|
73
87
|
- lib/apn_sender.rb
|
74
|
-
- CHANGELOG.md
|
75
|
-
- LICENSE
|
76
|
-
- README.md
|
77
|
-
- Rakefile
|
78
88
|
homepage: http://github.com/arthurnn/apn_sender
|
79
89
|
licenses:
|
80
90
|
- MIT
|
91
|
+
metadata: {}
|
81
92
|
post_install_message:
|
82
93
|
rdoc_options: []
|
83
94
|
require_paths:
|
84
95
|
- lib
|
85
96
|
required_ruby_version: !ruby/object:Gem::Requirement
|
86
|
-
none: false
|
87
97
|
requirements:
|
88
98
|
- - ">="
|
89
99
|
- !ruby/object:Gem::Version
|
90
100
|
version: '1.9'
|
91
101
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
92
|
-
none: false
|
93
102
|
requirements:
|
94
103
|
- - ">="
|
95
104
|
- !ruby/object:Gem::Version
|
96
105
|
version: 1.3.6
|
97
106
|
requirements: []
|
98
107
|
rubyforge_project:
|
99
|
-
rubygems_version:
|
108
|
+
rubygems_version: 2.5.1
|
100
109
|
signing_key:
|
101
|
-
specification_version:
|
110
|
+
specification_version: 4
|
102
111
|
summary: Background worker to send Apple Push Notifications over a persistent TCP
|
103
112
|
socket.
|
104
113
|
test_files: []
|
data/CHANGELOG.md
DELETED
@@ -1,28 +0,0 @@
|
|
1
|
-
# Version 2.0
|
2
|
-
## 2.0.2
|
3
|
-
- Add default file so we dont need to require 'apn' anymore
|
4
|
-
- Change backend switch:
|
5
|
-
Use simple backend per default, also allow changes.
|
6
|
-
Now you can change the backend using:
|
7
|
-
APN.backend = :sidekiq
|
8
|
-
|
9
|
-
## 2.0.1
|
10
|
-
- Use bytesize to truncate alert when necessary
|
11
|
-
- Better calculation on payload size. (botvinik)
|
12
|
-
- Fix generating payload should use bytesize. (piotr-sokolowski)
|
13
|
-
- Rescuing and repairing broken connections (Arseniy Ivanov)
|
14
|
-
|
15
|
-
## 2.0.0
|
16
|
-
- adding connection_pool for handle apple sockets
|
17
|
-
- removing resque hard dependency
|
18
|
-
- adding support for sending sync messages
|
19
|
-
- adding Thread support
|
20
|
-
- adding support to sidekiq (Caue Guerra)
|
21
|
-
- truncation messages when payload is greater than 256 option (Caue Guerra)
|
22
|
-
|
23
|
-
# Version 1.0
|
24
|
-
## 1.0.6
|
25
|
-
- Added support for password-protected .pem files
|
26
|
-
- Read feedback data in 38-byte chunks
|
27
|
-
- Support passing dictionary as :alert key
|
28
|
-
- Logging to STDOUT if no other loggers present
|
data/LICENSE
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
Copyright (c) 2013 Arthur Nogueira Neves
|
2
|
-
|
3
|
-
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
-
a copy of this software and associated documentation files (the
|
5
|
-
"Software"), to deal in the Software without restriction, including
|
6
|
-
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
-
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
-
permit persons to whom the Software is furnished to do so, subject to
|
9
|
-
the following conditions:
|
10
|
-
|
11
|
-
The above copyright notice and this permission notice shall be
|
12
|
-
included in all copies or substantial portions of the Software.
|
13
|
-
|
14
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
-
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
-
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
-
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
-
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
-
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
DELETED
@@ -1,151 +0,0 @@
|
|
1
|
-
[![Code Climate](http://img.shields.io/codeclimate/github/arthurnn/apn_sender.svg)](https://codeclimate.com/github/arthurnn/apn_sender)
|
2
|
-
[![Build Status](https://travis-ci.org/arthurnn/apn_sender.svg?branch=master)](https://travis-ci.org/arthurnn/apn_sender)
|
3
|
-
|
4
|
-
## Synopsis
|
5
|
-
|
6
|
-
Need to send background notifications to an iPhone application over a <em>persistent</em> connection in Ruby? Keep reading...
|
7
|
-
|
8
|
-
## The Story
|
9
|
-
|
10
|
-
So you're building the server component of an iPhone application in Ruby and you want to send background notifications through the Apple Push Notification servers. This doesn't seem too bad at first, but then you read in the [Apple Documentation](https://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Introduction.html) that Apple's servers may treat non-persistent connections as a Denial of Service attack. Since Rails has no easy way to maintain a persistent connection internally, things start to look complicated.
|
11
|
-
|
12
|
-
This gem includes a background daemon which processes background messages from your application and sends them along to Apple <em>over a single, persistent socket</em>. It also includes the ability to query the Feedback service, helper methods for enqueueing your jobs, and a sample monit config to make sure the background worker is around when you need it.
|
13
|
-
|
14
|
-
## Yet another ApplePushNotification interface?
|
15
|
-
|
16
|
-
Yup. There's some great code out there already, but we didn't like the idea of getting banned from the APN gateway for establishing a new connection each time we needed to send a batch of messages, and none of the libraries I found handled maintaining a persistent connection.
|
17
|
-
|
18
|
-
## Current Status
|
19
|
-
|
20
|
-
This gem has been used in production, on [500px](http://500px.com), sending hundreds of millions, if not, billions of notifications.
|
21
|
-
|
22
|
-
## Usage
|
23
|
-
|
24
|
-
APN sender can use [Resque](http://github.com/defunkt/resque) or [Sidekiq](https://github.com/mperham/sidekiq) to send asynchronous messages, if none of them are installed it creates a new thread to send messages.
|
25
|
-
|
26
|
-
### 1. Use a background processor or not.
|
27
|
-
|
28
|
-
You can either use Resque or Sidekiq, I strongly advice using Sidekiq, as apn_sender uses a connection pool for the apple socks. To use apn_sender with one of them you dont have to do anything, just include the background processor gem into your gemfile and it will all work.
|
29
|
-
|
30
|
-
### 2. Queueing Messages From Your Application
|
31
|
-
|
32
|
-
To queue a message for sending through Apple's Push Notification service from your Rails application:
|
33
|
-
|
34
|
-
```
|
35
|
-
APN.notify_async(token, opts_hash)
|
36
|
-
```
|
37
|
-
|
38
|
-
Where ```token``` is the unique identifier of the iPhone to receive the notification and ```opts_hash``` can have any of the following keys:
|
39
|
-
|
40
|
-
* :alert ## The alert to send
|
41
|
-
* :badge ## The badge number to send
|
42
|
-
* :sound ## The sound file to play on receipt, or true to play the default sound installed with your app
|
43
|
-
|
44
|
-
If any other keys are present they'll be be passed along as custom data to your application.
|
45
|
-
|
46
|
-
### 3. Sending Queued Messages
|
47
|
-
|
48
|
-
Put your ```apn_development.pem``` and ```apn_production.pem``` certificates from Apple in your ```RAILS_ROOT/config/certs``` directory.
|
49
|
-
|
50
|
-
You also can configure some extra settings:
|
51
|
-
|
52
|
-
```
|
53
|
-
APN.root = 'RAILS_ROOT/config/certs' # root to certificates folder
|
54
|
-
APN.certificate_name = 'apn_production.pem' # certificate filename
|
55
|
-
APN.host = 'apple host (on development sandbox url is used by default)'
|
56
|
-
APN.password = 'certificate_password'
|
57
|
-
APN.pool_size = 1 # number of connections on the pool
|
58
|
-
APN.pool_timeout = 5 # timeout in seconds for connection pool
|
59
|
-
APN.logger = Logger.new(File.join(Rails.root, 'log', 'apn_sender.log'))
|
60
|
-
```
|
61
|
-
|
62
|
-
Check ```logs/apn_sender.log``` for debugging output. In addition to logging any major errors there, apn_sender hooks into the Resque::Worker logging to display any verbose or very_verbose worker output in apn_sender.log file as well.
|
63
|
-
On latest versions apn_sender will use Rails.logger as the default logger.
|
64
|
-
|
65
|
-
|
66
|
-
### 4. Checking Apple's Feedback Service
|
67
|
-
|
68
|
-
Since push notifications are a fire-and-forget sorta deal, where you get no indication if your message was received (or if the specified recipient even exists), Apple needed to come up with some other way to ensure their network isn't clogged with thousands of bogus messages (e.g. from developers sending messages to phones where their application <em>used</em> to be installed, but where the user has since removed it). Hence, the Feedback Service.
|
69
|
-
|
70
|
-
It's actually really simple - you connect to them periodically and they give you a big dump of tokens you shouldn't send to anymore. The gem wraps this up nicely -- just call:
|
71
|
-
|
72
|
-
```
|
73
|
-
# APN::Feedback accepts the same optional :environment
|
74
|
-
# and :cert_path / :full_cert_path options as APN::Sender
|
75
|
-
feedback = APN::Feedback.new()
|
76
|
-
|
77
|
-
tokens = feedback.tokens # Array of device tokens
|
78
|
-
tokens.each do |token|
|
79
|
-
# ... custom logic here to stop you app from
|
80
|
-
# sending further notifications to this token
|
81
|
-
end
|
82
|
-
```
|
83
|
-
|
84
|
-
If you're interested in knowing exactly <em>when</em> Apple determined each token was expired (which can be useful in determining if the application re-registered with your service since it first appeared in the expired queue):
|
85
|
-
|
86
|
-
```
|
87
|
-
items = feedback.data # Array of APN::FeedbackItem elements
|
88
|
-
items.each do |item|
|
89
|
-
item.token
|
90
|
-
item.timestamp
|
91
|
-
# ... custom logic here
|
92
|
-
end
|
93
|
-
```
|
94
|
-
|
95
|
-
The Feedback Service works as a big queue. When you connect it pops off all its data and sends it over the wire at once, which means connecting a second time will return an empty array, so for ease of use a call to either +tokens+ or +data+ will connect once and cache the data. If you call either one again it'll continue to use its cached version (rather than connecting to Apple a second time to retrieve an empty array, which is probably not what you want).
|
96
|
-
|
97
|
-
Forcing a reconnect is as easy as calling either method with the single parameter +true+, but be sure you've already used the existing data because you'll never get it back.
|
98
|
-
|
99
|
-
|
100
|
-
#### Warning: No really, check Apple's Feedback Service occasionally
|
101
|
-
|
102
|
-
If you're sending notifications, you should definitely call one of the ```receive``` methods periodically, as Apple's policies require it and they apparently monitor providers for compliance. I'd definitely recommend throwing together a quick rake task to take care of this for you (the [whenever library](http://github.com/javan/whenever) provides a nice wrapper around scheduling tasks to run at certain times (for systems with cron enabled)).
|
103
|
-
|
104
|
-
Just for the record, this is essentially what you want to have whenever run periodically for you:
|
105
|
-
```
|
106
|
-
def self.clear_uninstalled_applications
|
107
|
-
feedback_data = APN::Feedback.new(:environment #> :production).data
|
108
|
-
|
109
|
-
feedback_data.each do |item|
|
110
|
-
user = User.find_by_iphone_token( item.token )
|
111
|
-
|
112
|
-
if user.iphone_token_updated_at && user.iphone_token_updated_at > item.timestamp
|
113
|
-
return true # App has been reregistered since Apple determined it'd been uninstalled
|
114
|
-
else
|
115
|
-
user.update_attributes(iphone_token: nil, iphone_token_updated_at: Time.now)
|
116
|
-
end
|
117
|
-
end
|
118
|
-
end
|
119
|
-
```
|
120
|
-
|
121
|
-
|
122
|
-
### Keeping Your Workers Working
|
123
|
-
|
124
|
-
There's also an included sample ```apn_sender.monitrc``` file in the ```contrib/``` folder to help monit handle server restarts and unexpected disasters.
|
125
|
-
|
126
|
-
|
127
|
-
## Installation
|
128
|
-
|
129
|
-
Add this line to your application's Gemfile:
|
130
|
-
|
131
|
-
gem 'apn_sender', require: 'apn'
|
132
|
-
|
133
|
-
And then execute:
|
134
|
-
|
135
|
-
$ bundle
|
136
|
-
|
137
|
-
Or install it yourself as:
|
138
|
-
|
139
|
-
$ gem install apn_sender
|
140
|
-
|
141
|
-
To add a few useful rake tasks for running workers, add the following line to your Rakefile:
|
142
|
-
|
143
|
-
```
|
144
|
-
require 'apn/tasks'
|
145
|
-
```
|
146
|
-
|
147
|
-
## License
|
148
|
-
|
149
|
-
APN Sender is released under the [MIT License](http://www.opensource.org/licenses/MIT).
|
150
|
-
|
151
|
-
|
data/Rakefile
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
require "bundler"
|
2
|
-
Bundler.setup
|
3
|
-
|
4
|
-
require "rake"
|
5
|
-
require "rspec/core/rake_task"
|
6
|
-
|
7
|
-
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
|
8
|
-
require "apn/version"
|
9
|
-
|
10
|
-
task :gem => :build
|
11
|
-
task :build do
|
12
|
-
system "gem build apn_sender.gemspec"
|
13
|
-
end
|
14
|
-
|
15
|
-
task :install => :build do
|
16
|
-
system "gem install apn_sender-#{APN::VERSION}.gem"
|
17
|
-
end
|
18
|
-
|
19
|
-
task :release => :build do
|
20
|
-
system "git tag -a v#{APN::VERSION} -m 'Tagging #{APN::VERSION}'"
|
21
|
-
system "git push --tags"
|
22
|
-
system "gem push apn_sender-#{APN::VERSION}.gem"
|
23
|
-
system "rm apn_sender-#{APN::VERSION}.gem"
|
24
|
-
end
|
25
|
-
|
26
|
-
RSpec::Core::RakeTask.new("spec") do |spec|
|
27
|
-
spec.pattern = "spec/**/*_spec.rb"
|
28
|
-
end
|
29
|
-
|
30
|
-
RSpec::Core::RakeTask.new('spec:progress') do |spec|
|
31
|
-
spec.rspec_opts = %w(--format progress)
|
32
|
-
spec.pattern = "spec/**/*_spec.rb"
|
33
|
-
end
|
34
|
-
|
35
|
-
task :default => :spec
|