houston 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile.lock CHANGED
@@ -1,11 +1,17 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- houston (0.0.1)
4
+ houston (0.1.0)
5
+ commander (~> 4.1.2)
6
+ json (~> 1.7.3)
5
7
 
6
8
  GEM
7
9
  remote: http://rubygems.org/
8
10
  specs:
11
+ commander (4.1.2)
12
+ highline (~> 1.6.11)
13
+ highline (1.6.13)
14
+ json (1.7.5)
9
15
  rake (0.9.2.2)
10
16
  rspec (0.6.4)
11
17
 
data/README.md CHANGED
@@ -3,35 +3,51 @@
3
3
 
4
4
  > Houston, We Have Liftoff!
5
5
 
6
- Push Notifications don't have to be difficult. And now they aren't.
6
+ Push Notifications don't have to be difficult.
7
7
 
8
8
  Houston is a simple gem for sending Apple Push Notifications. Pass your credentials, construct your message, and send it.
9
9
 
10
10
  In a production application, you will probably want to schedule or queue notifications into a background job. Whether you're using [queue_classic](https://github.com/ryandotsmith/queue_classic), [resque](https://github.com/defunkt/resque), or rolling you own infrastructure, integrating Houston couldn't be simpler.
11
11
 
12
- Another caveat is that Houston doesn't manage device tokens for you. Since infrastructures can vary dramatically for these kinds of things, being agnostic and not forcing any conventions here is more a feature than a bug. That said, a simple web service adapter, similar to [Rack::CoreData](https://github.com/mattt/rack-core-data) may be in the cards.
12
+ Another caveat is that Houston doesn't manage device tokens for you. Infrastructures can vary dramatically for these kinds of things, so being agnostic and not forcing any conventions here is more a feature than a bug, perhaps. Treat it the same way as you would an e-mail address, associating one or many for each user account.
13
+
14
+ _That said, a simple web service adapter, similar to [Rack::CoreData](https://github.com/mattt/rack-core-data) may be in the cards._
15
+
16
+ ## Installation
17
+
18
+ ```
19
+ $ gem install houston
20
+ ```
13
21
 
14
22
  ## Usage
15
23
 
16
24
  ```ruby
17
- # Environment variables are automatically read, or can be overridden by any specified options
18
- APN = Houston::Client.new
19
- APN.certificate = File.read("/path/to/apple_push_notification.pem")
20
-
21
- # An example of the token sent back when a device registers for notifications
22
- token = "<42e33061 9695b86b 59e5452b 061774c9 726eb8ec 22982c4b 558e34e2 784adee0>"
23
-
24
- # Create a notification that alerts a message to the user, plays a sound, and sets the badge on the app
25
- notification = Houston::Notification.new(device: token)
26
- notification.alert = "Hello, World!"
27
-
28
- # Notifications can also change the badge count, have a custom sound, or pass along arbitrary data.
29
- notification.badge = 57
30
- notification.sound = "sosumi.aiff"
31
- notification.custom_data = {foo: "bar"}
32
-
33
- # And... sent! That's all it takes.
34
- APN.push(notification)
25
+ # Environment variables are automatically read, or can be overridden by any specified options
26
+ APN = Houston::Client.new
27
+ APN.certificate = File.read("/path/to/apple_push_notification.pem")
28
+
29
+ # An example of the token sent back when a device registers for notifications
30
+ token = "<ce8be627 2e43e855 16033e24 b4c28922 0eeda487 9c477160 b2545e95 b68b5969>"
31
+
32
+ # Create a notification that alerts a message to the user, plays a sound, and sets the badge on the app
33
+ notification = Houston::Notification.new(device: token)
34
+ notification.alert = "Hello, World!"
35
+
36
+ # Notifications can also change the badge count, have a custom sound, or pass along arbitrary data.
37
+ notification.badge = 57
38
+ notification.sound = "sosumi.aiff"
39
+ notification.custom_data = {foo: "bar"}
40
+
41
+ # And... sent! That's all it takes.
42
+ APN.push(notification)
43
+ ```
44
+
45
+ ## Command Line Tool
46
+
47
+ Houston also comes with the `apn` binary, which provides a convenient way to test notifications from the command line.
48
+
49
+ ```
50
+ $ apn push "<token>" -c /path/to/apple_push_notification.pem -m "Hello from the command line!"
35
51
  ```
36
52
 
37
53
  ## Converting Your Certificate
@@ -48,13 +64,9 @@ and the apple certificate as p12 files. Here is a quick walkthrough on how to do
48
64
  Now covert the p12 file to a pem file:
49
65
 
50
66
  ```
51
- $ openssl pkcs12 -in cert.p12 -out apple_push_notification_production.pem -nodes -clcerts
67
+ $ openssl pkcs12 -in cert.p12 -out apple_push_notification.pem -nodes -clcerts
52
68
  ```
53
69
 
54
- If you are using a development certificate, then change the name to apple_push_notification_development.pem instead.
55
-
56
- Store the contents of the certificate files on the app model for the app you want to send notifications to.
57
-
58
70
  ## Contact
59
71
 
60
72
  Mattt Thompson
data/Rakefile CHANGED
@@ -7,5 +7,5 @@ task :build => "#{gemspec.full_name}.gem"
7
7
 
8
8
  file "#{gemspec.full_name}.gem" => gemspec.files + ["houston.gemspec"] do
9
9
  system "gem build houston.gemspec"
10
- system "gem install houston-#{Houston::Verson}.gem"
10
+ system "gem install houston-#{Houston::VERSION}.gem"
11
11
  end
data/bin/apn ADDED
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'commander/import'
4
+
5
+ require 'houston'
6
+
7
+ HighLine.track_eof = false # Fix for built-in Ruby
8
+ Signal.trap("INT") {} # Suppress backtrace when exiting command
9
+
10
+ program :version, Houston::VERSION
11
+ program :description, 'A command-line interface for sending push notifications'
12
+
13
+ program :help, 'Author', 'Mattt Thompson <m@mattt.me>'
14
+ program :help, 'Website', 'https://github.com/mattt'
15
+ program :help_formatter, :compact
16
+
17
+ default_command :help
18
+
19
+ command :push do |c|
20
+ c.syntax = 'apn push DEVICE [...]'
21
+ c.summary = 'Sends an Apple Push Notification to specified devices'
22
+ c.description = ''
23
+
24
+ c.example 'description', 'apn push <token> -m "Hello, World" -b 57 -s sosumi.aiff'
25
+ c.option '-m', '--alert ALERT', 'Body of the alert to send in the push notification'
26
+ c.option '-b', '--badge NUMBER', 'Badge number to set with the push notification'
27
+ c.option '-s', '--sound SOUND', 'Sound to play with the notification'
28
+ c.option '-e', '--environment [production|development]', 'Environment to send push notification (defaults to development)'
29
+ c.option '-c', '--certificate CERTIFICATE', 'Path to certificate (.pem) file'
30
+ c.option '-p', '--[no]-passphrase', 'Prompt for a certificate passphrase'
31
+
32
+ c.action do |args, options|
33
+ say_error "One or more device tokens required" and abort if args.empty?
34
+
35
+ @environment = options.environment.downcase.to_sym rescue :development
36
+ say_error "Invalid environment,'#{@environment}' (should be either :development or :production)" and abort unless [:development, :production].include?(@environment)
37
+
38
+ @certificate = options.certificate
39
+ say_error "Missing certificate file option (-c /path/to/certificate.pem)" and abort unless @certificate
40
+ say_error "Could not find certificate file '#{@certificate}'" and abort unless File.exists?(@certificate)
41
+
42
+ @passphrase = options.passphrase ? password : ""
43
+
44
+ @alert = options.alert
45
+ @badge = options.badge.nil? ? nil : options.badge.to_i
46
+ @sound = options.sound
47
+
48
+ unless @alert or @badge
49
+ placeholder = "Enter your alert message"
50
+ @alert = ask_editor placeholder
51
+ say_error "Alert message or badge required" and abort if @alert.nil? or @alert == placeholder
52
+ end
53
+
54
+ notifications = []
55
+ args.each do |token|
56
+ notification = Houston::Notification.new(device: token)
57
+ notification.alert = @alert
58
+ notification.badge = @badge
59
+ notification.sound = @sound
60
+
61
+ notifications << notification
62
+ end
63
+
64
+ client = @environment == :production ? Houston::Client.production : Houston::Client.development
65
+ client.certificate = File.read(@certificate)
66
+ client.passphrase = @passphrase
67
+
68
+ begin
69
+ client.push(*notifications)
70
+ rescue => e
71
+ say_error "Exception sending notification: #{e}" and abort
72
+ end
73
+
74
+ say_ok "Push notifications successfully sent"
75
+ end
76
+ end
data/houston-0.1.0.gem ADDED
File without changes
data/houston.gemspec CHANGED
@@ -7,10 +7,13 @@ Gem::Specification.new do |s|
7
7
  s.authors = ["Mattt Thompson"]
8
8
  s.email = "m@mattt.me"
9
9
  s.homepage = "http://github.com/mattt/houston"
10
- s.version = Houston::Version
10
+ s.version = Houston::VERSION
11
11
  s.platform = Gem::Platform::RUBY
12
- s.summary = "iOS Push Notifications"
13
- s.description = ""
12
+ s.summary = "Send Apple Push Notifications"
13
+ s.description = "Houston is a simple gem for sending Apple Push Notifications. Pass your credentials, construct your message, and send it."
14
+
15
+ s.add_dependency "commander", "~> 4.1.2"
16
+ s.add_dependency "json", "~> 1.7.3"
14
17
 
15
18
  s.add_development_dependency "rspec", "~> 0.6.1"
16
19
  s.add_development_dependency "rake", "~> 0.9.2"
data/lib/houston.rb CHANGED
@@ -1,147 +1,7 @@
1
- require 'uri'
2
- require 'socket'
3
- require 'openssl'
4
- require 'json'
5
-
6
1
  module Houston
7
- Version = "0.0.1"
8
-
9
- APPLE_PRODUCTION_GATEWAY_URI = "apn://gateway.push.apple.com:2195"
10
- APPLE_PRODUCTION_FEEDBACK_URI = "apn://feedback.push.apple.com:2196"
11
-
12
- APPLE_DEVELOPMENT_GATEWAY_URI = "apn://gateway.sandbox.push.apple.com:2195"
13
- APPLE_DEVELOPMENT_FEEDBACK_URI = "apn://feedback.push.apple.com:2196"
14
-
15
- class Client
16
- attr_accessor :gateway_uri, :feedback_uri, :certificate, :passphrase
17
-
18
- def initialize
19
- @gateway_uri = ENV['APN_GATEWAY_URI']
20
- @feedback_uri = ENV['APN_FEEDBACK_URI']
21
- @certificate = ENV['APN_CERTIFICATE']
22
- @passphrase = ENV['APN_CERTIFICATE_PASSPHRASE']
23
- end
24
-
25
- def self.development
26
- client = self.new
27
- client.gateway_uri = APPLE_DEVELOPMENT_GATEWAY_URI
28
- client.feedback_uri = APPLE_DEVELOPMENT_FEEDBACK_URI
29
- client
30
- end
31
-
32
- def self.production
33
- client = self.new
34
- client.gateway_uri = APPLE_PRODUCTION_GATEWAY_URI
35
- client.feedback_uri = APPLE_PRODUCTION_FEEDBACK_URI
36
- client
37
- end
38
-
39
- def push(*notifications)
40
- Connection.open(connection_options_for_endpoint(:gateway)) do |connection, socket|
41
- notifications.each do |notification|
42
- next if notification.sent?
43
-
44
- connection.write(notification.message)
45
- notification.mark_as_sent!
46
- end
47
- end
48
- end
49
-
50
- def devices
51
- devices = []
52
-
53
- Connection.open(connection_options_for_endpoint(:feedback)) do |connection, socket|
54
- while line = connection.read(38)
55
- feedback = line.unpack('N1n1H140')
56
- token = feedback[2].scan(/.{0,8}/).join(' ').strip
57
- devices << token if token
58
- end
59
- end
60
-
61
- devices
62
- end
63
-
64
- private
65
-
66
- def connection_options_for_endpoint(endpoint = :gateway)
67
- uri = case endpoint
68
- when :gateway then URI(@gateway_uri)
69
- when :feedback then URI(@feedback_uri)
70
- else
71
- raise ArgumentError
72
- end
73
-
74
- {
75
- certificate: @certificate,
76
- passphrase: @passphrase,
77
- host: uri.host,
78
- port: uri.port
79
- }
80
- end
81
- end
82
-
83
- class Notification
84
- attr_accessor :device, :alert, :badge, :sound, :custom_data
85
- attr_reader :sent_at
86
-
87
- def initialize(options = {})
88
- @device = options.delete(:device)
89
- @alert = options.delete(:alert)
90
- @badge = options.delete(:badge)
91
- @sound = options.delete(:sound)
92
-
93
- @custom_data = options
94
- end
95
-
96
- def payload
97
- json = {}.merge(@custom_data || {})
98
- json['aps'] = {}
99
- json['aps']['alert'] = @alert
100
- json['aps']['badge'] = @badge.to_i rescue 0
101
- json['aps']['sound'] = @sound
102
-
103
- json
104
- end
105
-
106
- def message
107
- json = payload.to_json
108
-
109
- "\0\0 #{[@device.gsub(/[<\s>]/, '')].pack('H*')}\0#{json.length.chr}#{json}"
110
- end
111
-
112
- def mark_as_sent!
113
- @sent_at = Time.now
114
- end
115
-
116
- def sent?
117
- !!@sent_at
118
- end
119
- end
120
-
121
- class Connection
122
- class << self
123
- def open(options = {})
124
- return unless block_given?
125
-
126
- [:certificate, :passphrase, :host, :port].each do |option|
127
- raise ArgumentError, "Missing connection parameter: #{option}" unless option
128
- end
129
-
130
- socket = TCPSocket.new(options[:host], options[:port])
131
-
132
- context = OpenSSL::SSL::SSLContext.new
133
- context.key = OpenSSL::PKey::RSA.new(options[:certificate], options[:passphrase])
134
- context.cert = OpenSSL::X509::Certificate.new(options[:certificate])
135
-
136
- ssl = OpenSSL::SSL::SSLSocket.new(socket, context)
137
- ssl.sync = true
138
- ssl.connect
139
-
140
- yield ssl, socket
141
-
142
- ssl.close
143
- socket.close
144
- end
145
- end
146
- end
2
+ VERSION = "0.1.0"
147
3
  end
4
+
5
+ require 'houston/client'
6
+ require 'houston/notification'
7
+ require 'houston/connection'
@@ -0,0 +1,75 @@
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"
4
+
5
+ APPLE_DEVELOPMENT_GATEWAY_URI = "apn://gateway.sandbox.push.apple.com:2195"
6
+ APPLE_DEVELOPMENT_FEEDBACK_URI = "apn://feedback.push.apple.com:2196"
7
+
8
+ class Client
9
+ attr_accessor :gateway_uri, :feedback_uri, :certificate, :passphrase
10
+
11
+ def initialize
12
+ @gateway_uri = ENV['APN_GATEWAY_URI']
13
+ @feedback_uri = ENV['APN_FEEDBACK_URI']
14
+ @certificate = ENV['APN_CERTIFICATE']
15
+ @passphrase = ENV['APN_CERTIFICATE_PASSPHRASE']
16
+ end
17
+
18
+ def self.development
19
+ client = self.new
20
+ client.gateway_uri = APPLE_DEVELOPMENT_GATEWAY_URI
21
+ client.feedback_uri = APPLE_DEVELOPMENT_FEEDBACK_URI
22
+ client
23
+ end
24
+
25
+ def self.production
26
+ client = self.new
27
+ client.gateway_uri = APPLE_PRODUCTION_GATEWAY_URI
28
+ client.feedback_uri = APPLE_PRODUCTION_FEEDBACK_URI
29
+ client
30
+ end
31
+
32
+ def push(*notifications)
33
+ Connection.open(connection_options_for_endpoint(:gateway)) do |connection, socket|
34
+ notifications.each do |notification|
35
+ next if notification.sent?
36
+
37
+ connection.write(notification.message)
38
+ notification.mark_as_sent!
39
+ end
40
+ end
41
+ end
42
+
43
+ def devices
44
+ devices = []
45
+
46
+ Connection.open(connection_options_for_endpoint(:feedback)) do |connection, socket|
47
+ while line = connection.read(38)
48
+ feedback = line.unpack('N1n1H140')
49
+ token = feedback[2].scan(/.{0,8}/).join(' ').strip
50
+ devices << token if token
51
+ end
52
+ end
53
+
54
+ devices
55
+ end
56
+
57
+ private
58
+
59
+ def connection_options_for_endpoint(endpoint = :gateway)
60
+ uri = case endpoint
61
+ when :gateway then URI(@gateway_uri)
62
+ when :feedback then URI(@feedback_uri)
63
+ else
64
+ raise ArgumentError
65
+ end
66
+
67
+ {
68
+ certificate: @certificate,
69
+ passphrase: @passphrase,
70
+ host: uri.host,
71
+ port: uri.port
72
+ }
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,32 @@
1
+ require 'uri'
2
+ require 'socket'
3
+ require 'openssl'
4
+
5
+ module Houston
6
+ class Connection
7
+ class << self
8
+ def open(options = {})
9
+ return unless block_given?
10
+
11
+ [:certificate, :passphrase, :host, :port].each do |option|
12
+ raise ArgumentError, "Missing connection parameter: #{option}" unless option
13
+ end
14
+
15
+ socket = TCPSocket.new(options[:host], options[:port])
16
+
17
+ context = OpenSSL::SSL::SSLContext.new
18
+ context.key = OpenSSL::PKey::RSA.new(options[:certificate], options[:passphrase])
19
+ context.cert = OpenSSL::X509::Certificate.new(options[:certificate])
20
+
21
+ ssl = OpenSSL::SSL::SSLSocket.new(socket, context)
22
+ ssl.sync = true
23
+ ssl.connect
24
+
25
+ yield ssl, socket
26
+
27
+ ssl.close
28
+ socket.close
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,41 @@
1
+ require 'json'
2
+
3
+ module Houston
4
+ class Notification
5
+ attr_accessor :device, :alert, :badge, :sound, :custom_data
6
+ attr_reader :sent_at
7
+
8
+ def initialize(options = {})
9
+ @device = options.delete(:device)
10
+ @alert = options.delete(:alert)
11
+ @badge = options.delete(:badge)
12
+ @sound = options.delete(:sound)
13
+
14
+ @custom_data = options
15
+ end
16
+
17
+ def payload
18
+ json = {}.merge(@custom_data || {})
19
+ json['aps'] = {}
20
+ json['aps']['alert'] = @alert
21
+ json['aps']['badge'] = @badge.to_i rescue 0
22
+ json['aps']['sound'] = @sound
23
+
24
+ json
25
+ end
26
+
27
+ def message
28
+ json = payload.to_json
29
+
30
+ "\0\0 #{[@device.gsub(/[<\s>]/, '')].pack('H*')}\0#{json.length.chr}#{json}"
31
+ end
32
+
33
+ def mark_as_sent!
34
+ @sent_at = Time.now
35
+ end
36
+
37
+ def sent?
38
+ !!@sent_at
39
+ end
40
+ end
41
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: houston
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -11,9 +11,31 @@ bindir: bin
11
11
  cert_chain: []
12
12
  date: 2012-09-07 00:00:00.000000000Z
13
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: commander
16
+ requirement: &70209585432560 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 4.1.2
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70209585432560
25
+ - !ruby/object:Gem::Dependency
26
+ name: json
27
+ requirement: &70209585432040 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: 1.7.3
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70209585432040
14
36
  - !ruby/object:Gem::Dependency
15
37
  name: rspec
16
- requirement: &70324475112900 !ruby/object:Gem::Requirement
38
+ requirement: &70209585431420 !ruby/object:Gem::Requirement
17
39
  none: false
18
40
  requirements:
19
41
  - - ~>
@@ -21,10 +43,10 @@ dependencies:
21
43
  version: 0.6.1
22
44
  type: :development
23
45
  prerelease: false
24
- version_requirements: *70324475112900
46
+ version_requirements: *70209585431420
25
47
  - !ruby/object:Gem::Dependency
26
48
  name: rake
27
- requirement: &70324475112400 !ruby/object:Gem::Requirement
49
+ requirement: &70209585430780 !ruby/object:Gem::Requirement
28
50
  none: false
29
51
  requirements:
30
52
  - - ~>
@@ -32,21 +54,28 @@ dependencies:
32
54
  version: 0.9.2
33
55
  type: :development
34
56
  prerelease: false
35
- version_requirements: *70324475112400
36
- description: ''
57
+ version_requirements: *70209585430780
58
+ description: Houston is a simple gem for sending Apple Push Notifications. Pass your
59
+ credentials, construct your message, and send it.
37
60
  email: m@mattt.me
38
- executables: []
61
+ executables:
62
+ - apn
39
63
  extensions: []
40
64
  extra_rdoc_files: []
41
65
  files:
42
66
  - ./apple_push_notification_production.pem
43
67
  - ./Gemfile
44
68
  - ./Gemfile.lock
69
+ - ./houston-0.1.0.gem
45
70
  - ./houston.gemspec
71
+ - ./lib/houston/client.rb
72
+ - ./lib/houston/connection.rb
73
+ - ./lib/houston/notification.rb
46
74
  - ./lib/houston.rb
47
75
  - ./LICENSE
48
76
  - ./Rakefile
49
77
  - ./README.md
78
+ - bin/apn
50
79
  homepage: http://github.com/mattt/houston
51
80
  licenses: []
52
81
  post_install_message:
@@ -61,7 +90,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
61
90
  version: '0'
62
91
  segments:
63
92
  - 0
64
- hash: 1752936197030512533
93
+ hash: 4053213412686882167
65
94
  required_rubygems_version: !ruby/object:Gem::Requirement
66
95
  none: false
67
96
  requirements:
@@ -70,11 +99,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
70
99
  version: '0'
71
100
  segments:
72
101
  - 0
73
- hash: 1752936197030512533
102
+ hash: 4053213412686882167
74
103
  requirements: []
75
104
  rubyforge_project:
76
105
  rubygems_version: 1.8.15
77
106
  signing_key:
78
107
  specification_version: 3
79
- summary: iOS Push Notifications
108
+ summary: Send Apple Push Notifications
80
109
  test_files: []