houston 0.2.4 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/Gemfile.lock +28 -8
  4. data/LICENSE +1 -1
  5. data/README.md +123 -24
  6. data/Rakefile +4 -8
  7. data/bin/apn +70 -5
  8. data/coverage/assets/0.7.1/application.css +1110 -0
  9. data/coverage/assets/0.7.1/application.js +626 -0
  10. data/coverage/assets/0.7.1/fancybox/blank.gif +0 -0
  11. data/coverage/assets/0.7.1/fancybox/fancy_close.png +0 -0
  12. data/coverage/assets/0.7.1/fancybox/fancy_loading.png +0 -0
  13. data/coverage/assets/0.7.1/fancybox/fancy_nav_left.png +0 -0
  14. data/coverage/assets/0.7.1/fancybox/fancy_nav_right.png +0 -0
  15. data/coverage/assets/0.7.1/fancybox/fancy_shadow_e.png +0 -0
  16. data/coverage/assets/0.7.1/fancybox/fancy_shadow_n.png +0 -0
  17. data/coverage/assets/0.7.1/fancybox/fancy_shadow_ne.png +0 -0
  18. data/coverage/assets/0.7.1/fancybox/fancy_shadow_nw.png +0 -0
  19. data/coverage/assets/0.7.1/fancybox/fancy_shadow_s.png +0 -0
  20. data/coverage/assets/0.7.1/fancybox/fancy_shadow_se.png +0 -0
  21. data/coverage/assets/0.7.1/fancybox/fancy_shadow_sw.png +0 -0
  22. data/coverage/assets/0.7.1/fancybox/fancy_shadow_w.png +0 -0
  23. data/coverage/assets/0.7.1/fancybox/fancy_title_left.png +0 -0
  24. data/coverage/assets/0.7.1/fancybox/fancy_title_main.png +0 -0
  25. data/coverage/assets/0.7.1/fancybox/fancy_title_over.png +0 -0
  26. data/coverage/assets/0.7.1/fancybox/fancy_title_right.png +0 -0
  27. data/coverage/assets/0.7.1/fancybox/fancybox-x.png +0 -0
  28. data/coverage/assets/0.7.1/fancybox/fancybox-y.png +0 -0
  29. data/coverage/assets/0.7.1/fancybox/fancybox.png +0 -0
  30. data/coverage/assets/0.7.1/favicon_green.png +0 -0
  31. data/coverage/assets/0.7.1/favicon_red.png +0 -0
  32. data/coverage/assets/0.7.1/favicon_yellow.png +0 -0
  33. data/coverage/assets/0.7.1/loading.gif +0 -0
  34. data/coverage/assets/0.7.1/magnify.png +0 -0
  35. data/coverage/assets/0.7.1/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  36. data/coverage/assets/0.7.1/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
  37. data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  38. data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  39. data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  40. data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  41. data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  42. data/coverage/assets/0.7.1/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  43. data/coverage/assets/0.7.1/smoothness/images/ui-icons_222222_256x240.png +0 -0
  44. data/coverage/assets/0.7.1/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
  45. data/coverage/assets/0.7.1/smoothness/images/ui-icons_454545_256x240.png +0 -0
  46. data/coverage/assets/0.7.1/smoothness/images/ui-icons_888888_256x240.png +0 -0
  47. data/coverage/assets/0.7.1/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
  48. data/coverage/assets/0.8.0/application.css +799 -0
  49. data/coverage/assets/0.8.0/application.js +1559 -0
  50. data/coverage/assets/0.8.0/colorbox/border.png +0 -0
  51. data/coverage/assets/0.8.0/colorbox/controls.png +0 -0
  52. data/coverage/assets/0.8.0/colorbox/loading.gif +0 -0
  53. data/coverage/assets/0.8.0/colorbox/loading_background.png +0 -0
  54. data/coverage/assets/0.8.0/favicon_green.png +0 -0
  55. data/coverage/assets/0.8.0/favicon_red.png +0 -0
  56. data/coverage/assets/0.8.0/favicon_yellow.png +0 -0
  57. data/coverage/assets/0.8.0/loading.gif +0 -0
  58. data/coverage/assets/0.8.0/magnify.png +0 -0
  59. data/coverage/assets/0.8.0/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  60. data/coverage/assets/0.8.0/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
  61. data/coverage/assets/0.8.0/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  62. data/coverage/assets/0.8.0/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  63. data/coverage/assets/0.8.0/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  64. data/coverage/assets/0.8.0/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  65. data/coverage/assets/0.8.0/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  66. data/coverage/assets/0.8.0/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  67. data/coverage/assets/0.8.0/smoothness/images/ui-icons_222222_256x240.png +0 -0
  68. data/coverage/assets/0.8.0/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
  69. data/coverage/assets/0.8.0/smoothness/images/ui-icons_454545_256x240.png +0 -0
  70. data/coverage/assets/0.8.0/smoothness/images/ui-icons_888888_256x240.png +0 -0
  71. data/coverage/assets/0.8.0/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
  72. data/coverage/index.html +1810 -0
  73. data/houston-2.2.2.gem +0 -0
  74. data/houston-2.2.3.gem +0 -0
  75. data/houston.gemspec +17 -16
  76. data/lib/houston/client.rb +39 -9
  77. data/lib/houston/connection.rb +3 -3
  78. data/lib/houston/notification.rb +87 -6
  79. data/lib/houston/version.rb +3 -0
  80. data/lib/houston.rb +1 -4
  81. data/spec/client_spec.rb +100 -0
  82. data/spec/notification_spec.rb +315 -0
  83. data/spec/spec_helper.rb +31 -0
  84. metadata +119 -32
  85. data/apple_push_notification_production.pem +0 -136
data/houston-2.2.2.gem ADDED
Binary file
data/houston-2.2.3.gem ADDED
Binary file
data/houston.gemspec CHANGED
@@ -1,26 +1,27 @@
1
1
  # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path("../lib", __FILE__)
3
- require "houston"
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+ require 'houston/version'
4
4
 
5
5
  Gem::Specification.new do |s|
6
- s.name = "houston"
7
- s.authors = ["Mattt Thompson"]
8
- s.email = "m@mattt.me"
9
- s.license = "MIT"
10
- s.homepage = "http://github.com/mattt/houston"
6
+ s.name = 'houston'
7
+ s.authors = ['Mattt Thompson']
8
+ s.email = 'm@mattt.me'
9
+ s.license = 'MIT'
10
+ s.homepage = 'http://nomad-cli.com'
11
11
  s.version = Houston::VERSION
12
12
  s.platform = Gem::Platform::RUBY
13
- s.summary = "Send Apple Push Notifications"
14
- s.description = "Houston is a simple gem for sending Apple Push Notifications. Pass your credentials, construct your message, and send it."
13
+ s.summary = 'Send Apple Push Notifications'
14
+ s.description = 'Houston is a simple gem for sending Apple Push Notifications. Pass your credentials, construct your message, and send it.'
15
15
 
16
- s.add_dependency "commander", "~> 4.1"
17
- s.add_dependency "json"
16
+ s.add_dependency 'commander', '~> 4.4'
17
+ s.add_dependency 'json'
18
18
 
19
- s.add_development_dependency "rspec", "~> 0.6"
20
- s.add_development_dependency "rake", "~> 0.9"
19
+ s.add_development_dependency 'rspec', '~> 3.5'
20
+ s.add_development_dependency 'rake'
21
+ s.add_development_dependency 'simplecov'
21
22
 
22
- s.files = Dir["./**/*"].reject { |file| file =~ /\.\/(bin|log|pkg|script|spec|test|vendor)/ }
23
+ s.files = Dir['./**/*'].reject { |file| file =~ /\.\/(bin|log|pkg|script|spec|test|vendor)/ }
23
24
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
24
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
25
- s.require_paths = ["lib"]
25
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
26
+ s.require_paths = ['lib']
26
27
  end
@@ -1,12 +1,12 @@
1
1
  module Houston
2
- APPLE_PRODUCTION_GATEWAY_URI = "apn://gateway.push.apple.com:2195"
3
- APPLE_PRODUCTION_FEEDBACK_URI = "apn://feedback.push.apple.com:2196"
2
+ APPLE_PRODUCTION_GATEWAY_URI = 'apn://gateway.push.apple.com:2195'
3
+ APPLE_PRODUCTION_FEEDBACK_URI = 'apn://feedback.push.apple.com:2196'
4
4
 
5
- APPLE_DEVELOPMENT_GATEWAY_URI = "apn://gateway.sandbox.push.apple.com:2195"
6
- APPLE_DEVELOPMENT_FEEDBACK_URI = "apn://feedback.sandbox.push.apple.com:2196"
5
+ APPLE_DEVELOPMENT_GATEWAY_URI = 'apn://gateway.sandbox.push.apple.com:2195'
6
+ APPLE_DEVELOPMENT_FEEDBACK_URI = 'apn://feedback.sandbox.push.apple.com:2196'
7
7
 
8
8
  class Client
9
- attr_accessor :gateway_uri, :feedback_uri, :certificate, :passphrase
9
+ attr_accessor :gateway_uri, :feedback_uri, :certificate, :passphrase, :timeout
10
10
 
11
11
  class << self
12
12
  def development
@@ -27,36 +27,66 @@ module Houston
27
27
  def initialize
28
28
  @gateway_uri = ENV['APN_GATEWAY_URI']
29
29
  @feedback_uri = ENV['APN_FEEDBACK_URI']
30
- @certificate = ENV['APN_CERTIFICATE']
30
+ @certificate = certificate_data
31
31
  @passphrase = ENV['APN_CERTIFICATE_PASSPHRASE']
32
+ @timeout = Float(ENV['APN_TIMEOUT'] || 0.5)
32
33
  end
33
34
 
34
35
  def push(*notifications)
35
36
  return if notifications.empty?
36
37
 
38
+ notifications.flatten!
39
+
37
40
  Connection.open(@gateway_uri, @certificate, @passphrase) do |connection|
38
- notifications.flatten.each do |notification|
41
+ ssl = connection.ssl
42
+
43
+ notifications.each_with_index do |notification, index|
39
44
  next unless notification.kind_of?(Notification)
40
45
  next if notification.sent?
46
+ next unless notification.valid?
47
+
48
+ notification.id = index
41
49
 
42
50
  connection.write(notification.message)
43
51
  notification.mark_as_sent!
52
+
53
+ read_socket, _write_socket = IO.select([ssl], [ssl], [ssl], nil)
54
+ if (read_socket && read_socket[0])
55
+ if error = connection.read(6)
56
+ _command, status, index = error.unpack('ccN')
57
+ notification.apns_error_code = status
58
+ notification.mark_as_unsent!
59
+ end
60
+ end
44
61
  end
45
62
  end
46
63
  end
47
64
 
48
- def devices
65
+ def unregistered_devices
49
66
  devices = []
50
67
 
51
68
  Connection.open(@feedback_uri, @certificate, @passphrase) do |connection|
52
69
  while line = connection.read(38)
53
70
  feedback = line.unpack('N1n1H140')
71
+ timestamp = feedback[0]
54
72
  token = feedback[2].scan(/.{0,8}/).join(' ').strip
55
- devices << token if token
73
+ devices << { token: token, timestamp: timestamp } if token && timestamp
56
74
  end
57
75
  end
58
76
 
59
77
  devices
60
78
  end
79
+
80
+ def devices
81
+ unregistered_devices.collect { |device| device[:token] }
82
+ end
83
+
84
+ def certificate_data
85
+ if ENV['APN_CERTIFICATE']
86
+ File.read(ENV['APN_CERTIFICATE'])
87
+ elsif ENV['APN_CERTIFICATE_DATA']
88
+ ENV['APN_CERTIFICATE_DATA']
89
+ end
90
+ end
61
91
  end
62
92
  end
@@ -26,8 +26,8 @@ module Houston
26
26
 
27
27
  def initialize(uri, certificate, passphrase)
28
28
  @uri = URI(uri)
29
- @certificate = certificate
30
- @passphrase = passphrase
29
+ @certificate = certificate.to_s
30
+ @passphrase = passphrase.to_s unless passphrase.nil?
31
31
  end
32
32
 
33
33
  def open
@@ -45,7 +45,7 @@ module Houston
45
45
  end
46
46
 
47
47
  def open?
48
- not (@ssl and @socket).nil?
48
+ not (@ssl && @socket).nil?
49
49
  end
50
50
 
51
51
  def close
@@ -2,8 +2,37 @@ require 'json'
2
2
 
3
3
  module Houston
4
4
  class Notification
5
- attr_accessor :token, :alert, :badge, :sound, :content_available, :custom_data
5
+ class APNSError < RuntimeError
6
+ # See: https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW12
7
+ CODES = {
8
+ 0 => 'No errors encountered',
9
+ 1 => 'Processing error',
10
+ 2 => 'Missing device token',
11
+ 3 => 'Missing topic',
12
+ 4 => 'Missing payload',
13
+ 5 => 'Invalid token size',
14
+ 6 => 'Invalid topic size',
15
+ 7 => 'Invalid payload size',
16
+ 8 => 'Invalid token',
17
+ 10 => 'Shutdown',
18
+ 255 => 'Unknown error'
19
+ }
20
+
21
+ attr_reader :code
22
+
23
+ def initialize(code)
24
+ raise ArgumentError unless CODES.include?(code)
25
+ super(CODES[code])
26
+ @code = code
27
+ end
28
+ end
29
+
30
+ MAXIMUM_PAYLOAD_SIZE = 2048
31
+
32
+ attr_accessor :token, :alert, :badge, :sound, :category, :content_available, :mutable_content,
33
+ :custom_data, :id, :expiry, :priority, :url_args, :thread_id
6
34
  attr_reader :sent_at
35
+ attr_writer :apns_error_code
7
36
 
8
37
  alias :device :token
9
38
  alias :device= :token=
@@ -13,35 +42,87 @@ module Houston
13
42
  @alert = options.delete(:alert)
14
43
  @badge = options.delete(:badge)
15
44
  @sound = options.delete(:sound)
45
+ @category = options.delete(:category)
46
+ @expiry = options.delete(:expiry)
47
+ @id = options.delete(:id)
48
+ @priority = options.delete(:priority)
16
49
  @content_available = options.delete(:content_available)
50
+ @mutable_content = options.delete(:mutable_content)
51
+ @url_args = options.delete(:url_args)
52
+ @thread_id = options.delete(:thread_id)
17
53
 
18
54
  @custom_data = options
55
+
56
+ @sent_at = nil
57
+ @apns_error_code = nil
19
58
  end
20
59
 
21
60
  def payload
22
- json = {}.merge(@custom_data || {})
61
+ json = {}.merge(@custom_data || {}).inject({}) { |h, (k, v)| h[k.to_s] = v; h }
62
+
23
63
  json['aps'] ||= {}
24
64
  json['aps']['alert'] = @alert if @alert
25
65
  json['aps']['badge'] = @badge.to_i rescue 0 if @badge
26
66
  json['aps']['sound'] = @sound if @sound
67
+ json['aps']['category'] = @category if @category
27
68
  json['aps']['content-available'] = 1 if @content_available
69
+ json['aps']['mutable-content'] = 1 if @mutable_content
70
+ json['aps']['url-args'] = @url_args if @url_args
71
+ json['aps']['thread-id'] = @thread_id if @thread_id
28
72
 
29
73
  json
30
74
  end
31
75
 
32
76
  def message
33
- json = payload.to_json
34
- hex_token = [@token.gsub(/[<\s>]/, '')].pack('H*')
35
-
36
- [0, 0, 32, hex_token, 0, json.bytes.count, json].pack('ccca*cca*')
77
+ data = [device_token_item,
78
+ payload_item,
79
+ identifier_item,
80
+ expiration_item,
81
+ priority_item].compact.join
82
+ [2, data.bytes.count, data].pack('cNa*')
37
83
  end
38
84
 
39
85
  def mark_as_sent!
40
86
  @sent_at = Time.now
41
87
  end
42
88
 
89
+ def mark_as_unsent!
90
+ @sent_at = nil
91
+ end
92
+
43
93
  def sent?
44
94
  !!@sent_at
45
95
  end
96
+
97
+ def valid?
98
+ payload.to_json.bytesize <= MAXIMUM_PAYLOAD_SIZE
99
+ end
100
+
101
+ def error
102
+ APNSError.new(@apns_error_code) if @apns_error_code && @apns_error_code.nonzero?
103
+ end
104
+
105
+ private
106
+
107
+ def device_token_item
108
+ [1, 32, @token.gsub(/[<\s>]/, '')].pack('cnH64')
109
+ end
110
+
111
+ def payload_item
112
+ json = payload.to_json
113
+ [2, json.bytes.count, json].pack('cna*')
114
+ end
115
+
116
+ def identifier_item
117
+ [3, 4, @id].pack('cnN') unless @id.nil?
118
+ end
119
+
120
+ def expiration_item
121
+ [4, 4, @expiry.to_i].pack('cnN') unless @expiry.nil?
122
+ end
123
+
124
+ def priority_item
125
+ [5, 1, @priority].pack('cnc') unless @priority.nil?
126
+ end
46
127
  end
47
128
  end
@@ -0,0 +1,3 @@
1
+ module Houston
2
+ VERSION = '2.4.0'
3
+ end
data/lib/houston.rb CHANGED
@@ -1,7 +1,4 @@
1
- module Houston
2
- VERSION = "0.2.4"
3
- end
4
-
1
+ require 'houston/version'
5
2
  require 'houston/client'
6
3
  require 'houston/notification'
7
4
  require 'houston/connection'
@@ -0,0 +1,100 @@
1
+ require 'spec_helper'
2
+
3
+ describe Houston::Client do
4
+ subject { Houston::Client.development }
5
+
6
+ before(:each) do
7
+ stub_const('Houston::Connection', MockConnection)
8
+ end
9
+
10
+ context '#development' do
11
+ subject { Houston::Client.development }
12
+
13
+ describe '#gateway_uri' do
14
+ subject { super().gateway_uri }
15
+ it { should == Houston::APPLE_DEVELOPMENT_GATEWAY_URI }
16
+ end
17
+
18
+ describe '#feedback_uri' do
19
+ subject { super().feedback_uri }
20
+ it { should == Houston::APPLE_DEVELOPMENT_FEEDBACK_URI }
21
+ end
22
+ end
23
+
24
+ context '#production' do
25
+ subject { Houston::Client.production }
26
+
27
+ describe '#gateway_uri' do
28
+ subject { super().gateway_uri }
29
+ it { should == Houston::APPLE_PRODUCTION_GATEWAY_URI }
30
+ end
31
+
32
+ describe '#feedback_uri' do
33
+ subject { super().feedback_uri }
34
+ it { should == Houston::APPLE_PRODUCTION_FEEDBACK_URI }
35
+ end
36
+ end
37
+
38
+ context '#new' do
39
+ context 'passing options through ENV' do
40
+ ENV['APN_GATEWAY_URI'] = 'apn://gateway.example.com'
41
+ ENV['APN_FEEDBACK_URI'] = 'apn://feedback.example.com'
42
+ ENV['APN_CERTIFICATE_PASSPHRASE'] = 'passphrase'
43
+ ENV['APN_TIMEOUT'] = '10.0'
44
+
45
+ subject do
46
+ Houston::Client.new
47
+ end
48
+
49
+ describe '#gateway_uri' do
50
+ subject { super().gateway_uri }
51
+ it { should == ENV['APN_GATEWAY_URI'] }
52
+ end
53
+
54
+ describe '#feedback_uri' do
55
+ subject { super().feedback_uri }
56
+ it { should == ENV['APN_FEEDBACK_URI'] }
57
+ end
58
+
59
+ describe '#certificate' do
60
+ subject { super().certificate }
61
+ it { should be_nil }
62
+ end
63
+
64
+ describe '#passphrase' do
65
+ subject { super().passphrase }
66
+ it { should == ENV['APN_CERTIFICATE_PASSPHRASE'] }
67
+ end
68
+
69
+ describe '#timeout' do
70
+ subject { super().timeout }
71
+ it { should be_a(Float) }
72
+ it { should == Float(ENV['APN_TIMEOUT']) }
73
+ end
74
+ end
75
+
76
+ describe '#push' do
77
+ it 'should accept zero arguments' do
78
+ expect(Houston::Client.development.push()).to be_nil()
79
+ end
80
+ end
81
+ end
82
+
83
+ describe '#unregistered_devices' do
84
+ it 'should correctly parse the feedback response and create a dictionary of unregistered devices with timestamps' do
85
+ expect(subject.unregistered_devices).to eq [
86
+ { token: 'ce8be627 2e43e855 16033e24 b4c28922 0eeda487 9c477160 b2545e95 b68b5969', timestamp: 443779200 },
87
+ { token: 'ce8be627 2e43e855 16033e24 b4c28922 0eeda487 9c477160 b2545e95 b68b5970', timestamp: 1388678223 }
88
+ ]
89
+ end
90
+ end
91
+
92
+ describe '#devices' do
93
+ it 'should correctly parse the feedback response and create an array of unregistered devices' do
94
+ expect(subject.devices).to eq [
95
+ 'ce8be627 2e43e855 16033e24 b4c28922 0eeda487 9c477160 b2545e95 b68b5969',
96
+ 'ce8be627 2e43e855 16033e24 b4c28922 0eeda487 9c477160 b2545e95 b68b5970'
97
+ ]
98
+ end
99
+ end
100
+ end