pling 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +1 -0
- data/Guardfile +4 -0
- data/README.md +8 -7
- data/lib/pling/adapter/base.rb +2 -0
- data/lib/pling/apn/connection.rb +122 -0
- data/lib/pling/apn/feedback.rb +88 -0
- data/lib/pling/{gateway/apn.rb → apn/gateway.rb} +38 -31
- data/lib/pling/apn.rb +13 -0
- data/lib/pling/{gateway/c2dm.rb → c2dm/gateway.rb} +27 -9
- data/lib/pling/c2dm.rb +17 -0
- data/lib/pling/delayed_initializer.rb +3 -1
- data/lib/pling/gateway.rb +120 -4
- data/lib/pling/message.rb +52 -0
- data/lib/pling/middleware/base.rb +31 -0
- data/lib/pling/version.rb +1 -1
- data/lib/pling.rb +24 -2
- data/pling.gemspec +1 -1
- data/spec/pling/.DS_Store +0 -0
- data/spec/pling/adapter/.DS_Store +0 -0
- data/spec/pling/apn/connection_spec.rb +124 -0
- data/spec/pling/apn/feedback_spec.rb +44 -0
- data/spec/pling/apn/gateway_spec.rb +141 -0
- data/spec/pling/{gateway/c2dm_spec.rb → c2dm/gateway_spec.rb} +79 -20
- data/spec/pling/gateway_spec.rb +65 -3
- data/spec/pling/message_spec.rb +25 -0
- data/spec/pling_spec.rb +9 -0
- metadata +31 -22
- data/lib/pling/gateway/base.rb +0 -64
- data/spec/pling/gateway/apn_spec.rb +0 -103
- data/spec/pling/gateway/base_spec.rb +0 -61
data/Gemfile
CHANGED
data/Guardfile
CHANGED
data/README.md
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
Pling is a notification framework that supports multiple gateways. This gem implements the basic framework as well as a gateway to Google's Cloud to Device Messaging Service (C2DM) and Apple's Push Notification Service (APN).
|
4
4
|
|
5
|
+
See the [API Documentation](http://rubydoc.info/github/flinc/pling/master/file/README.md) for more in depth documentation.
|
5
6
|
|
6
7
|
## Requirements
|
7
8
|
|
@@ -24,8 +25,8 @@ Add this line to your `Gemfile`:
|
|
24
25
|
The configuration is pretty simple. Just add a configuration block like this to your code:
|
25
26
|
|
26
27
|
Pling.configure do |config|
|
27
|
-
config.gateways.use Pling::Gateway
|
28
|
-
config.gateways.use Pling::Gateway
|
28
|
+
config.gateways.use Pling::C2DM::Gateway, :email => 'your-email@gmail.com', :password => 'your-password', :source => 'your-app-name'
|
29
|
+
config.gateways.use Pling::APN::Gateway, :certificate => '/path/to/certificate.pem'
|
29
30
|
|
30
31
|
# config.middleware.use Your::Custom::Middleware, :your => :custom, :configuration => true
|
31
32
|
|
@@ -40,7 +41,7 @@ After configuring Pling you can send messages to devices by like this:
|
|
40
41
|
device = Pling::Device.new(:identifier => 'XXXXXXXXXX...XXXXXX', :type => :iphone)
|
41
42
|
device.deliver(message)
|
42
43
|
|
43
|
-
# ... or call Pling.
|
44
|
+
# ... or call Pling.deliver
|
44
45
|
Pling.deliver(message, device)
|
45
46
|
|
46
47
|
Pling has three core components:
|
@@ -77,12 +78,12 @@ The Gateway delivers the message in the required format to the service provider.
|
|
77
78
|
|
78
79
|
Currently there are these gateways available:
|
79
80
|
|
80
|
-
* [Android C2DM](http://rdoc.info/github/flinc/pling/master/Pling/Gateway
|
81
|
-
* [Apple Push Notification](http://rdoc.info/github/flinc/pling/master/Pling/Gateway
|
81
|
+
* [Android C2DM](http://rdoc.info/github/flinc/pling/master/Pling/C2DM/Gateway)
|
82
|
+
* [Apple Push Notification](http://rdoc.info/github/flinc/pling/master/Pling/APN/Gateway)
|
82
83
|
* [SMS via Mobilant](https://github.com/flinc/pling-mobilant) (See `pling-mobilant` gem)
|
83
84
|
* [Email](https://github.com/flinc/pling-actionmailer) (See `pling-actionmailer` gem)
|
84
85
|
|
85
|
-
See the [API documentation](http://
|
86
|
+
See the [API documentation](http://rubydoc.info/github/flinc/pling/master/file/README.md) for details on the available gateways.
|
86
87
|
|
87
88
|
|
88
89
|
### Middleware
|
@@ -106,7 +107,7 @@ Pling has support for middlewares. Currently pling itself does not provide any m
|
|
106
107
|
You can either add middlewares for all gateways or for specific gateways:
|
107
108
|
|
108
109
|
Pling.configure do |config|
|
109
|
-
config.gateways.use Pling::Gateway
|
110
|
+
config.gateways.use Pling::APN::Gateway, {
|
110
111
|
:certificate => '/path/to/certificate.pem',
|
111
112
|
:middlewares => [
|
112
113
|
[Pling::Middleware::TimeFilter, { :range => 9..17 }] # Don't deliver any messages to iOS devices between 9am and 5pm
|
data/lib/pling/adapter/base.rb
CHANGED
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module Pling
|
4
|
+
module APN
|
5
|
+
class Connection
|
6
|
+
include Pling::Configurable
|
7
|
+
|
8
|
+
def initialize(config)
|
9
|
+
setup_configuration(config, :require => [:certificate])
|
10
|
+
open
|
11
|
+
end
|
12
|
+
|
13
|
+
def open
|
14
|
+
Pling.logger.info "#{self.class} -- Opening connection in #{Process.pid}"
|
15
|
+
ssl_socket.connect
|
16
|
+
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def reopen
|
21
|
+
close
|
22
|
+
open
|
23
|
+
end
|
24
|
+
|
25
|
+
def open?
|
26
|
+
not closed?
|
27
|
+
end
|
28
|
+
|
29
|
+
def close
|
30
|
+
Pling.logger.info "#{self.class} -- Closing connection in #{Process.pid}"
|
31
|
+
ssl_socket.close rescue true
|
32
|
+
tcp_socket.close rescue true
|
33
|
+
|
34
|
+
@ssl_socket = @tcp_socket = nil
|
35
|
+
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
def closed?
|
40
|
+
return true if ssl_socket.closed?
|
41
|
+
tcp_socket.read_nonblock(1)
|
42
|
+
return false
|
43
|
+
rescue Errno::EAGAIN
|
44
|
+
return false
|
45
|
+
rescue Exception => e
|
46
|
+
return !e.message.match(/read would block/)
|
47
|
+
end
|
48
|
+
|
49
|
+
def write(*args, &block)
|
50
|
+
with_retries do
|
51
|
+
raise IOError, "Connection closed" if closed?
|
52
|
+
ssl_socket.write(*args, &block)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def puts(*args, &block)
|
57
|
+
with_retries do
|
58
|
+
raise IOError, "Connection closed" if closed?
|
59
|
+
ssl_socket.puts(*args, &block)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def read(*args, &block)
|
64
|
+
with_retries do
|
65
|
+
raise IOError, "Connection closed" if closed?
|
66
|
+
ssl_socket.read(*args, &block)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def gets(*args, &block)
|
71
|
+
with_retries do
|
72
|
+
raise IOError, "Connection closed" if closed?
|
73
|
+
ssl_socket.gets(*args, &block)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
protected
|
78
|
+
|
79
|
+
def ssl_context
|
80
|
+
@ssl_context ||= OpenSSL::SSL::SSLContext.new.tap do |context|
|
81
|
+
certificate = File.read(configuration[:certificate])
|
82
|
+
|
83
|
+
context.cert = OpenSSL::X509::Certificate.new(certificate)
|
84
|
+
context.key = OpenSSL::PKey::RSA.new(certificate)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def tcp_socket
|
89
|
+
@tcp_socket ||= TCPSocket.new(configuration[:host], configuration[:port]).tap do |tcp_socket|
|
90
|
+
tcp_socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
|
91
|
+
tcp_socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def ssl_socket
|
96
|
+
@ssl_socket ||= OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context) do |ssl_socket|
|
97
|
+
ssl_socket.sync = true
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def default_configuration
|
104
|
+
super.merge(
|
105
|
+
:host => 'gateway.push.apple.com',
|
106
|
+
:port => 2195
|
107
|
+
)
|
108
|
+
end
|
109
|
+
|
110
|
+
def with_retries(count = 3)
|
111
|
+
yield
|
112
|
+
rescue OpenSSL::SSL::SSLError, SocketError, SystemCallError, IOError
|
113
|
+
if (count -= 1) > 0
|
114
|
+
Pling.logger.info "#{self.class} -- #{$!.message}"
|
115
|
+
reopen; retry
|
116
|
+
else
|
117
|
+
raise IOError, $!.message
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Pling
|
2
|
+
module APN
|
3
|
+
|
4
|
+
##
|
5
|
+
# Instances of this class can be used to retrieve device identifiers that
|
6
|
+
# have been marked invalid. This should be done on a regular basis since
|
7
|
+
# Apple will ban you if you continue to send push notifications to
|
8
|
+
# invalid devices.
|
9
|
+
#
|
10
|
+
# The only operation supported by instances of this class is {#get}.
|
11
|
+
# The method simply returns a list of the identifieres that have been
|
12
|
+
# marked invalid since you last called this method.
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
#
|
16
|
+
# feedback = Pling::APN::Feedback.new(:certificate => '/path/to/certificate.pem')
|
17
|
+
# tokens = feedback.get
|
18
|
+
#
|
19
|
+
# tokens.each do |token|
|
20
|
+
# # process token
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
class Feedback
|
24
|
+
include Pling::Configurable
|
25
|
+
|
26
|
+
##
|
27
|
+
# Creates a new instance of this class and establishes a connection to
|
28
|
+
# Apple's Push Notification Service.
|
29
|
+
#
|
30
|
+
# The connection is only established once since Apple will ban you if
|
31
|
+
# you reconnect each time you want to retrieve the list of invalid
|
32
|
+
# device identifiers. For testing purposes, you should use Apple's
|
33
|
+
# sandbox feedback service +feedback.sandbox.push.apple.com+. In order
|
34
|
+
# to do this, you have to specify the optional +:host+ parameter when
|
35
|
+
# creating instances of this class.
|
36
|
+
#
|
37
|
+
# @param [Hash] configuration Parameters to control the connection configuration
|
38
|
+
# @option configuration [#to_s] :certificate Path to PEM certificate file (Required)
|
39
|
+
# @option configuration [String] :host Host to connect to (Default: feedback.push.apple.com)
|
40
|
+
# @option configuration [Integer] :port Port to connect to (Default: 2196)
|
41
|
+
#
|
42
|
+
# @example
|
43
|
+
#
|
44
|
+
# Pling::APN::Feedback.new(
|
45
|
+
# :certificate => '/path/to/certificate.pem',
|
46
|
+
# :host => 'feedback.push.apple.com',
|
47
|
+
# :port => 2196
|
48
|
+
# )
|
49
|
+
#
|
50
|
+
def initialize(configuration)
|
51
|
+
setup_configuration(configuration, :require => :certificate)
|
52
|
+
end
|
53
|
+
|
54
|
+
##
|
55
|
+
# Retrieves all device identifiers that have been marked invalid since
|
56
|
+
# the method has been called last.
|
57
|
+
#
|
58
|
+
# @return [Array<String>] The list of invalid device identifiers
|
59
|
+
#
|
60
|
+
def get
|
61
|
+
tokens = []
|
62
|
+
while line = connection.gets
|
63
|
+
time, length = line.unpack("Nn")
|
64
|
+
tokens << line.unpack("x6H#{length << 1}").first
|
65
|
+
end
|
66
|
+
tokens
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def connection
|
72
|
+
@connection ||= Connection.new(
|
73
|
+
:host => configuration[:host],
|
74
|
+
:port => configuration[:port],
|
75
|
+
:certificate => configuration[:certificate]
|
76
|
+
)
|
77
|
+
end
|
78
|
+
|
79
|
+
def default_configuration
|
80
|
+
super.merge(
|
81
|
+
:host => 'feedback.push.apple.com',
|
82
|
+
:port => 2196
|
83
|
+
)
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -3,21 +3,21 @@ require 'openssl'
|
|
3
3
|
require 'json'
|
4
4
|
|
5
5
|
module Pling
|
6
|
-
module
|
6
|
+
module APN
|
7
7
|
##
|
8
8
|
# Pling gateway to communicate with Apple's Push Notification service.
|
9
9
|
#
|
10
|
-
# This gateway handles these device types:
|
10
|
+
# This gateway handles these device types:
|
11
11
|
# :apple, :apn, :ios, :ipad, :iphone, :ipod
|
12
12
|
#
|
13
13
|
# Configure it by providing the path to your certificate:
|
14
14
|
#
|
15
|
-
# Pling::Gateway
|
15
|
+
# Pling::APN::Gateway.new({
|
16
16
|
# :certificate => '/path/to/certificate.pem', # Required
|
17
17
|
# :host => 'gateway.sandbox.push.apple.com' # Optional
|
18
18
|
# })
|
19
19
|
#
|
20
|
-
class
|
20
|
+
class Gateway < Pling::Gateway
|
21
21
|
handles :apple, :apn, :ios, :ipad, :iphone, :ipod
|
22
22
|
|
23
23
|
##
|
@@ -30,7 +30,13 @@ module Pling
|
|
30
30
|
def initialize(configuration)
|
31
31
|
super
|
32
32
|
require_configuration(:certificate)
|
33
|
-
|
33
|
+
setup!
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# Establishes a new connection if connection is not available or closed
|
38
|
+
def setup!
|
39
|
+
connection.reopen if connection.closed?
|
34
40
|
end
|
35
41
|
|
36
42
|
##
|
@@ -39,47 +45,48 @@ module Pling
|
|
39
45
|
# @param [#to_pling_message] message
|
40
46
|
# @param [#to_pling_device] device
|
41
47
|
def deliver!(message, device)
|
42
|
-
token = [device.identifier].pack('H*')
|
43
|
-
|
44
48
|
data = {
|
45
49
|
:aps => {
|
46
|
-
:alert => message.body
|
47
|
-
|
48
|
-
|
50
|
+
:alert => message.body,
|
51
|
+
:badge => message.badge && message.badge.to_i,
|
52
|
+
:sound => message.sound
|
53
|
+
}.delete_if { |_, value| value.nil? }
|
54
|
+
}
|
49
55
|
|
50
|
-
|
56
|
+
data.merge!(message.payload) if configuration[:payload] && message.payload
|
57
|
+
|
58
|
+
data = data.to_json
|
59
|
+
|
60
|
+
if data.bytesize > 256
|
61
|
+
raise Pling::DeliveryFailed.new(
|
62
|
+
"Payload size of #{data.bytesize} exceeds allowed size of 256 bytes.",
|
63
|
+
message,
|
64
|
+
device)
|
65
|
+
end
|
51
66
|
|
52
|
-
|
67
|
+
token = [device.identifier].pack('H*')
|
68
|
+
|
69
|
+
connection.write([0, token.bytesize, token, data.bytesize, data].pack('cna*na*'))
|
53
70
|
end
|
54
71
|
|
55
72
|
private
|
56
73
|
|
57
74
|
def default_configuration
|
58
|
-
super.merge(
|
75
|
+
super.merge(
|
59
76
|
:host => 'gateway.push.apple.com',
|
60
|
-
:port => 2195
|
61
|
-
|
77
|
+
:port => 2195,
|
78
|
+
:payload => false
|
79
|
+
)
|
62
80
|
end
|
63
81
|
|
64
82
|
def connection
|
65
|
-
@connection ||=
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
def ssl_context
|
72
|
-
@ssl_context ||= OpenSSL::SSL::SSLContext.new.tap do |context|
|
73
|
-
certificate = File.read(configuration[:certificate])
|
74
|
-
|
75
|
-
context.cert = OpenSSL::X509::Certificate.new(certificate)
|
76
|
-
context.key = OpenSSL::PKey::RSA.new(certificate)
|
77
|
-
end
|
83
|
+
@connection ||= Connection.new(
|
84
|
+
:host => configuration[:host],
|
85
|
+
:port => configuration[:port],
|
86
|
+
:certificate => configuration[:certificate]
|
87
|
+
)
|
78
88
|
end
|
79
89
|
|
80
|
-
def tcp_socket
|
81
|
-
@tcp_socket ||= TCPSocket.new(configuration[:host], configuration[:port])
|
82
|
-
end
|
83
90
|
end
|
84
91
|
end
|
85
92
|
end
|
data/lib/pling/apn.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
module Pling
|
2
|
+
|
3
|
+
##
|
4
|
+
# The {APN} module wraps Apple's Push Notification Service into an easy
|
5
|
+
# to use API. You can use instances of {Gateway} to send push notifications
|
6
|
+
# and instances of {Feedback} to get device identifiers that have been
|
7
|
+
# marked invalid.
|
8
|
+
module APN
|
9
|
+
autoload :Connection, 'pling/apn/connection'
|
10
|
+
autoload :Gateway, 'pling/apn/gateway'
|
11
|
+
autoload :Feedback, 'pling/apn/feedback'
|
12
|
+
end
|
13
|
+
end
|
@@ -1,16 +1,16 @@
|
|
1
1
|
require 'faraday'
|
2
2
|
|
3
3
|
module Pling
|
4
|
-
module
|
4
|
+
module C2DM
|
5
5
|
##
|
6
6
|
# Pling gateway to communicate with Google's Android C2DM service.
|
7
7
|
#
|
8
8
|
# The gateway is implemented using Faraday. It defaults to Faraday's :net_http adapter.
|
9
9
|
# You can customize the adapter by passing the :adapter configuration.
|
10
10
|
#
|
11
|
-
#
|
11
|
+
# @example
|
12
12
|
#
|
13
|
-
# Pling::Gateway
|
13
|
+
# Pling::C2DM::Gateway.new({
|
14
14
|
# :email => 'your-email@gmail.com', # Your google account's email address (Required)
|
15
15
|
# :password => 'your-password', # Your google account's password (Required)
|
16
16
|
# :source => 'your-app-name', # Your applications source identifier (Required)
|
@@ -20,7 +20,9 @@ module Pling
|
|
20
20
|
# :adapter => :net_http, # The Faraday adapter you want to use (Optional, Default: :net_http)
|
21
21
|
# :connection => {} # Options you want to pass to Faraday (Optional, Default: {})
|
22
22
|
# })
|
23
|
-
class
|
23
|
+
class Gateway < Pling::Gateway
|
24
|
+
|
25
|
+
handles :android, :c2dm
|
24
26
|
|
25
27
|
attr_reader :token
|
26
28
|
|
@@ -39,7 +41,11 @@ module Pling
|
|
39
41
|
def initialize(configuration)
|
40
42
|
super
|
41
43
|
require_configuration([:email, :password, :source])
|
42
|
-
|
44
|
+
setup!
|
45
|
+
end
|
46
|
+
|
47
|
+
def setup!
|
48
|
+
authenticate! unless token
|
43
49
|
end
|
44
50
|
|
45
51
|
##
|
@@ -49,14 +55,26 @@ module Pling
|
|
49
55
|
# @param [#to_pling_device] device
|
50
56
|
# @raise Pling::DeliveryFailed
|
51
57
|
def deliver!(message, device)
|
52
|
-
|
58
|
+
data = {
|
53
59
|
:registration_id => device.identifier,
|
54
|
-
:
|
60
|
+
:'data.body' => message.body,
|
61
|
+
:'data.badge' => message.badge,
|
62
|
+
:'data.sound' => message.sound,
|
63
|
+
:'data.subject' => message.subject,
|
55
64
|
:collapse_key => message.body.hash
|
56
|
-
}
|
65
|
+
}.delete_if { |_, value| value.nil? }
|
66
|
+
|
67
|
+
if configuration[:payload] && message.payload
|
68
|
+
message.payload.each do |key, value|
|
69
|
+
data["data.#{key}".to_sym] = value
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
response = connection.post(configuration[:push_url], data, { :Authorization => "GoogleLogin auth=#{@token}"})
|
57
74
|
|
58
75
|
if !response.success? || response.body =~ /^Error=(.+)$/
|
59
|
-
|
76
|
+
error_class = Pling::C2DM.const_get($1) rescue Pling::DeliveryFailed
|
77
|
+
raise error_class.new("C2DM Delivery failed: [#{response.status}] #{response.body}", message, device)
|
60
78
|
end
|
61
79
|
end
|
62
80
|
|
data/lib/pling/c2dm.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Pling
|
2
|
+
##
|
3
|
+
# This module adds support for Google's Cloud to Device Messaging (C2DM) to pling.
|
4
|
+
# Please refer to {Pling::C2DM::Gateway} for documentation.
|
5
|
+
#
|
6
|
+
# @see Pling::C2DM::Gateway
|
7
|
+
module C2DM
|
8
|
+
autoload :Gateway, 'pling/c2dm/gateway'
|
9
|
+
|
10
|
+
class QuotaExceeded < Pling::DeliveryFailed; end
|
11
|
+
class DeviceQuotaExceeded < Pling::DeliveryFailed; end
|
12
|
+
class InvalidRegistration < Pling::DeliveryFailed; end
|
13
|
+
class NotRegistered < Pling::DeliveryFailed; end
|
14
|
+
class MessageTooBig < Pling::DeliveryFailed; end
|
15
|
+
class MissingCollapseKey < Pling::DeliveryFailed; end
|
16
|
+
end
|
17
|
+
end
|
data/lib/pling/gateway.rb
CHANGED
@@ -1,10 +1,34 @@
|
|
1
1
|
module Pling
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
2
|
+
##
|
3
|
+
# This is the base class of all gateways. It defines the public interface of
|
4
|
+
# all gateways and provides helper methods to configure gateways and define
|
5
|
+
# the device types a gateway is able to handle.
|
6
|
+
#
|
7
|
+
# Gateway implementations must set the types they're able to handle by calling
|
8
|
+
# the {handles} macro.
|
9
|
+
#
|
10
|
+
# Every gateway must implement a {#deliver!} method which
|
11
|
+
# does the actual delivering.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
#
|
15
|
+
# class Pling::Example::Gateway < Pling::Gateway
|
16
|
+
# handles :example, :foo, :bar
|
17
|
+
#
|
18
|
+
# def deliver!(message, device)
|
19
|
+
# puts "Delivering #{message.body} to #{device.identifier} (#{device.type})"
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
class Gateway
|
23
|
+
include Pling::Configurable
|
6
24
|
|
7
25
|
class << self
|
26
|
+
##
|
27
|
+
# Finds a gateway that handles the given device
|
28
|
+
#
|
29
|
+
# @param device [#to_pling_device]
|
30
|
+
# @raise [Pling::NoGatewayFound] No gateway was found that is able to handle the given device
|
31
|
+
# @return [Pling::Gateway] A gateway that handles the given device
|
8
32
|
def discover(device)
|
9
33
|
device = Pling._convert(device, :device)
|
10
34
|
Pling.gateways.initialize!
|
@@ -12,6 +36,98 @@ module Pling
|
|
12
36
|
gateway.handles?(device)
|
13
37
|
end or raise(Pling::NoGatewayFound, "Could not find a gateway for #{device.class} with type :#{device.type}")
|
14
38
|
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Defines the device types a gateway is able to handle
|
42
|
+
#
|
43
|
+
# @param types [Array<#to_sym>] List of types
|
44
|
+
# @return [Array<#to_sym>] List of types
|
45
|
+
def handles(*types)
|
46
|
+
@handled_types = [types].flatten.map { |t| t.to_sym }
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# Returns a list of device types that this gateway is able to handle
|
51
|
+
#
|
52
|
+
# @return [Array<#to_sym>] List of types
|
53
|
+
def handled_types
|
54
|
+
@handled_types ||= []
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Initializes a new Gateway instance.
|
60
|
+
#
|
61
|
+
# @param [Hash] config Configuration for this gateway instance
|
62
|
+
# @option config [Array] :middlewares List of middlewares to execute before delivering
|
63
|
+
# @option config [#call(exception)] :on_exception Callback to execute when an exception is raised
|
64
|
+
def initialize(config = {})
|
65
|
+
setup_configuration(config)
|
66
|
+
middlewares = configuration[:middlewares]
|
67
|
+
configuration.merge!(:middlewares => Pling::DelayedInitializer.new)
|
68
|
+
middlewares.each { |middleware| configuration[:middlewares] << middleware } if middlewares
|
69
|
+
end
|
70
|
+
|
71
|
+
##
|
72
|
+
# Sets up this gateway
|
73
|
+
def setup!
|
74
|
+
end
|
75
|
+
|
76
|
+
##
|
77
|
+
# Tears down this gateway
|
78
|
+
def teardown!
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# Checks if this gateway is able to handle the given device
|
83
|
+
# @param device [#to_pling_device]
|
84
|
+
# @return [Boolean]
|
85
|
+
def handles?(device)
|
86
|
+
device = Pling._convert(device, :device)
|
87
|
+
self.class.handled_types.include?(device.type)
|
15
88
|
end
|
89
|
+
|
90
|
+
##
|
91
|
+
# Delivers the given message to the given device using the given stack.
|
92
|
+
# If the :on_exception callback is configured it'll rescue all Pling::Errors
|
93
|
+
# and pass them to the given callback.
|
94
|
+
#
|
95
|
+
# @param message [#to_pling_message]
|
96
|
+
# @param device [#to_pling_device]
|
97
|
+
# @param stack [Array] The stack to use (Default: configuration[:middlewares])
|
98
|
+
# @raise [Pling::DeliveryError] unless configuration[:on_exception] callback is set
|
99
|
+
def deliver(message, device, stack = nil)
|
100
|
+
message = Pling._convert(message, :message)
|
101
|
+
device = Pling._convert(device, :device)
|
102
|
+
|
103
|
+
stack ||= [] + configuration[:middlewares].initialize!
|
104
|
+
|
105
|
+
return deliver!(message, device) if stack.empty?
|
106
|
+
|
107
|
+
stack.shift.deliver(message, device) do |m, d|
|
108
|
+
deliver(m, d, stack)
|
109
|
+
end
|
110
|
+
rescue Pling::Error => error
|
111
|
+
callback = configuration[:on_exception]
|
112
|
+
callback && callback.respond_to?(:call) ? callback.call(error) : raise
|
113
|
+
end
|
114
|
+
|
115
|
+
##
|
116
|
+
# Delivers the given message to the given device without using the middleware.
|
117
|
+
#
|
118
|
+
# @param message [#to_pling_message]
|
119
|
+
# @param device [#to_pling_device]
|
120
|
+
# @raise [Pling::NotImplementedError] This method must be implemented in subclasses
|
121
|
+
def deliver!(message, device)
|
122
|
+
raise NotImplementedError, "Please implement #{self.class}#deliver!(message, device)"
|
123
|
+
end
|
124
|
+
|
125
|
+
protected
|
126
|
+
|
127
|
+
def default_configuration
|
128
|
+
{
|
129
|
+
:middlewares => Pling::DelayedInitializer.new
|
130
|
+
}
|
131
|
+
end
|
16
132
|
end
|
17
133
|
end
|