pling 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/Gemfile +4 -1
- data/Guardfile +4 -0
- data/README.md +82 -21
- data/Rakefile +6 -1
- data/lib/pling.rb +86 -1
- data/lib/pling/adapter.rb +5 -0
- data/lib/pling/adapter/base.rb +17 -0
- data/lib/pling/configurable.rb +29 -0
- data/lib/pling/delayed_initializer.rb +13 -0
- data/lib/pling/device.rb +68 -0
- data/lib/pling/gateway.rb +17 -0
- data/lib/pling/gateway/apn.rb +85 -0
- data/lib/pling/gateway/base.rb +64 -0
- data/lib/pling/gateway/c2dm.rb +102 -0
- data/lib/pling/message.rb +56 -0
- data/lib/pling/middleware.rb +5 -0
- data/lib/pling/middleware/base.rb +15 -0
- data/lib/pling/version.rb +1 -1
- data/pling.gemspec +4 -4
- data/spec/pling/adapter/base_spec.rb +22 -0
- data/spec/pling/delayed_initializer_spec.rb +31 -0
- data/spec/pling/device_spec.rb +75 -0
- data/spec/pling/gateway/apn_spec.rb +103 -0
- data/spec/pling/gateway/base_spec.rb +61 -0
- data/spec/pling/gateway/c2dm_spec.rb +182 -0
- data/spec/pling/gateway_spec.rb +50 -0
- data/spec/pling/message_spec.rb +44 -0
- data/spec/pling_spec.rb +117 -0
- metadata +64 -11
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'openssl'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Pling
|
6
|
+
module Gateway
|
7
|
+
##
|
8
|
+
# Pling gateway to communicate with Apple's Push Notification service.
|
9
|
+
#
|
10
|
+
# This gateway handles these device types:
|
11
|
+
# :apple, :apn, :ios, :ipad, :iphone, :ipod
|
12
|
+
#
|
13
|
+
# Configure it by providing the path to your certificate:
|
14
|
+
#
|
15
|
+
# Pling::Gateway::APN.new({
|
16
|
+
# :certificate => '/path/to/certificate.pem', # Required
|
17
|
+
# :host => 'gateway.sandbox.push.apple.com' # Optional
|
18
|
+
# })
|
19
|
+
#
|
20
|
+
class APN < Base
|
21
|
+
handles :apple, :apn, :ios, :ipad, :iphone, :ipod
|
22
|
+
|
23
|
+
##
|
24
|
+
# Initializes a new gateway to Apple's Push Notification service
|
25
|
+
#
|
26
|
+
# @param [Hash] configuration
|
27
|
+
# @option configuration [#to_s] :certificate Path to PEM certificate file (Required)
|
28
|
+
# @option configuration [String] :host Host to connect to (Default: gateway.push.apple.com)
|
29
|
+
# @option configuration [Integer] :port Port to connect to (Default: 2195)
|
30
|
+
def initialize(configuration)
|
31
|
+
super
|
32
|
+
require_configuration(:certificate)
|
33
|
+
connection
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# Sends the given message to the given device without using the middleware.
|
38
|
+
#
|
39
|
+
# @param [#to_pling_message] message
|
40
|
+
# @param [#to_pling_device] device
|
41
|
+
def deliver!(message, device)
|
42
|
+
token = [device.identifier].pack('H*')
|
43
|
+
|
44
|
+
data = {
|
45
|
+
:aps => {
|
46
|
+
:alert => message.body
|
47
|
+
}
|
48
|
+
}.to_json
|
49
|
+
|
50
|
+
raise Pling::DeliveryFailed, "Payload size of #{data.bytesize} exceeds allowed size of 256 bytes." if data.bytesize > 256
|
51
|
+
|
52
|
+
connection.write([0, 0, 32, token, 0, data.bytesize, data].pack('ccca*cca*'))
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def default_configuration
|
58
|
+
super.merge({
|
59
|
+
:host => 'gateway.push.apple.com',
|
60
|
+
:port => 2195
|
61
|
+
})
|
62
|
+
end
|
63
|
+
|
64
|
+
def connection
|
65
|
+
@connection ||= OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context).tap do |socket|
|
66
|
+
socket.sync = true
|
67
|
+
socket.connect
|
68
|
+
end
|
69
|
+
end
|
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
|
78
|
+
end
|
79
|
+
|
80
|
+
def tcp_socket
|
81
|
+
@tcp_socket ||= TCPSocket.new(configuration[:host], configuration[:port])
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Pling
|
2
|
+
module Gateway
|
3
|
+
class Base
|
4
|
+
include Pling::Configurable
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def handles(*types)
|
8
|
+
@handled_types = [types].flatten.map { |t| t.to_sym }
|
9
|
+
end
|
10
|
+
|
11
|
+
def handled_types
|
12
|
+
@handled_types ||= []
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(config = {})
|
17
|
+
setup_configuration(config)
|
18
|
+
middlewares = configuration[:middlewares]
|
19
|
+
configuration.merge!(:middlewares => Pling::DelayedInitializer.new)
|
20
|
+
middlewares.each { |middleware| configuration[:middlewares] << middleware } if middlewares
|
21
|
+
end
|
22
|
+
|
23
|
+
def handles?(device)
|
24
|
+
self.class.handled_types.include?(device.type)
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# Delivers the given message to the given device using the given stack.
|
29
|
+
#
|
30
|
+
# @param message [#to_pling_message]
|
31
|
+
# @param device [#to_pling_device]
|
32
|
+
# @param stack [Array] The stack to use (Default: configuration[:middlewares])
|
33
|
+
def deliver(message, device, stack = nil)
|
34
|
+
message = Pling._convert(message, :message)
|
35
|
+
device = Pling._convert(device, :device)
|
36
|
+
|
37
|
+
stack ||= [] + configuration[:middlewares].initialize!
|
38
|
+
|
39
|
+
return deliver!(message, device) if stack.empty?
|
40
|
+
|
41
|
+
stack.shift.deliver(message, device) do |m, d|
|
42
|
+
deliver(m, d, stack)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# Delivers the given message to the given device without using the middleware.
|
48
|
+
#
|
49
|
+
# @param message [#to_pling_message]
|
50
|
+
# @param device [#to_pling_device]
|
51
|
+
def deliver!(message, device)
|
52
|
+
raise NotImplementedError, "Please implement #{self.class}#deliver!(message, device)"
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
def default_configuration
|
58
|
+
{
|
59
|
+
:middlewares => Pling::DelayedInitializer.new
|
60
|
+
}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
|
3
|
+
module Pling
|
4
|
+
module Gateway
|
5
|
+
##
|
6
|
+
# Pling gateway to communicate with Google's Android C2DM service.
|
7
|
+
#
|
8
|
+
# The gateway is implemented using Faraday. It defaults to Faraday's :net_http adapter.
|
9
|
+
# You can customize the adapter by passing the :adapter configuration.
|
10
|
+
#
|
11
|
+
# Example:
|
12
|
+
#
|
13
|
+
# Pling::Gateway::C2DM.new({
|
14
|
+
# :email => 'your-email@gmail.com', # Your google account's email address (Required)
|
15
|
+
# :password => 'your-password', # Your google account's password (Required)
|
16
|
+
# :source => 'your-app-name', # Your applications source identifier (Required)
|
17
|
+
#
|
18
|
+
# :authentication_url => 'http://...', # The authentication url to use (Optional, Default: C2DM default authentication url)
|
19
|
+
# :push_url => 'http://...', # The push url to use (Optional, Default: C2DM default authentication url)
|
20
|
+
# :adapter => :net_http, # The Faraday adapter you want to use (Optional, Default: :net_http)
|
21
|
+
# :connection => {} # Options you want to pass to Faraday (Optional, Default: {})
|
22
|
+
# })
|
23
|
+
class C2DM < Base
|
24
|
+
|
25
|
+
attr_reader :token
|
26
|
+
|
27
|
+
##
|
28
|
+
# Initializes a new gateway to Apple's Push Notification service
|
29
|
+
#
|
30
|
+
# @param [Hash] configuration
|
31
|
+
# @option configuration [String] :email Your C2DM enabled Google account (Required)
|
32
|
+
# @option configuration [String] :password Your Google account's password (Required)
|
33
|
+
# @option configuration [String] :source Your applications identifier (Required)
|
34
|
+
# @option configuration [String] :authentication_url The URL to authenticate with (Optional)
|
35
|
+
# @option configuration [String] :push_url The URL to push to (Optional)
|
36
|
+
# @option configuration [Symbol] :adapter The Faraday adapter to use (Optional)
|
37
|
+
# @option configuration [String] :connection Any options for Faraday (Optional)
|
38
|
+
# @raise Pling::AuthenticationFailed
|
39
|
+
def initialize(configuration)
|
40
|
+
super
|
41
|
+
require_configuration([:email, :password, :source])
|
42
|
+
authenticate!
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Sends the given message to the given device.
|
47
|
+
#
|
48
|
+
# @param [#to_pling_message] message
|
49
|
+
# @param [#to_pling_device] device
|
50
|
+
# @raise Pling::DeliveryFailed
|
51
|
+
def deliver!(message, device)
|
52
|
+
response = connection.post(configuration[:push_url], {
|
53
|
+
:registration_id => device.identifier,
|
54
|
+
:"data.body" => message.body,
|
55
|
+
:collapse_key => message.body.hash
|
56
|
+
}, { :Authorization => "GoogleLogin auth=#{@token}"})
|
57
|
+
|
58
|
+
if !response.success? || response.body =~ /^Error=(.+)$/
|
59
|
+
raise(Pling::DeliveryFailed, "C2DM Delivery failed: [#{response.status}] #{response.body}")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def authenticate!
|
66
|
+
response = connection.post(configuration[:authentication_url], {
|
67
|
+
:accountType => 'HOSTED_OR_GOOGLE',
|
68
|
+
:service => 'ac2dm',
|
69
|
+
:Email => configuration[:email],
|
70
|
+
:Passwd => configuration[:password],
|
71
|
+
:source => configuration[:source]
|
72
|
+
})
|
73
|
+
|
74
|
+
raise(Pling::AuthenticationFailed, "C2DM Authentication failed: [#{response.status}] #{response.body}") unless response.success?
|
75
|
+
|
76
|
+
@token = extract_token(response.body)
|
77
|
+
end
|
78
|
+
|
79
|
+
def default_configuration
|
80
|
+
super.merge({
|
81
|
+
:authentication_url => 'https://www.google.com/accounts/ClientLogin',
|
82
|
+
:push_url => 'https://android.apis.google.com/c2dm/send',
|
83
|
+
:adapter => :net_http,
|
84
|
+
:connection => {}
|
85
|
+
})
|
86
|
+
end
|
87
|
+
|
88
|
+
def connection
|
89
|
+
@connection ||= Faraday.new(configuration[:connection]) do |builder|
|
90
|
+
builder.use Faraday::Request::UrlEncoded
|
91
|
+
builder.use Faraday::Response::Logger if configuration[:debug]
|
92
|
+
builder.adapter(configuration[:adapter])
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def extract_token(body)
|
97
|
+
matches = body.match(/^Auth=(.+)$/)
|
98
|
+
matches ? matches[1] : raise(Pling::AuthenticationFailed, "C2DM Token extraction failed")
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Pling
|
2
|
+
class Message
|
3
|
+
##
|
4
|
+
# The message body
|
5
|
+
#
|
6
|
+
# @overload body
|
7
|
+
# @overload body=(body)
|
8
|
+
# @param [#to_s] body
|
9
|
+
attr_reader :body
|
10
|
+
|
11
|
+
def body=(body)
|
12
|
+
body &&= body.to_s
|
13
|
+
@body = body
|
14
|
+
end
|
15
|
+
|
16
|
+
##
|
17
|
+
# Creates a new Pling::Message instance with the given body
|
18
|
+
#
|
19
|
+
# @overload initialize(body)
|
20
|
+
# @param [#to_s] body
|
21
|
+
# @overload initialize(attributes)
|
22
|
+
# @param [Hash] attributes
|
23
|
+
# @option attributes [#to_s] :body
|
24
|
+
def initialize(*args)
|
25
|
+
attributes = case param = args.shift
|
26
|
+
when String
|
27
|
+
(args.last || {}).merge(:body => param)
|
28
|
+
when Hash
|
29
|
+
param
|
30
|
+
else
|
31
|
+
{}
|
32
|
+
end
|
33
|
+
|
34
|
+
attributes.each_pair do |key, value|
|
35
|
+
method = "#{key}="
|
36
|
+
send(method, value) if respond_to?(method)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# A message is valid if it has a body.
|
42
|
+
#
|
43
|
+
# @return [Boolean]
|
44
|
+
def valid?
|
45
|
+
!!body
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Returns the object itself as it is already a Pling::Message.
|
50
|
+
#
|
51
|
+
# @return [Pling::Message]
|
52
|
+
def to_pling_message
|
53
|
+
self
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/pling/version.rb
CHANGED
data/pling.gemspec
CHANGED
@@ -5,7 +5,7 @@ require "pling/version"
|
|
5
5
|
Gem::Specification.new do |s|
|
6
6
|
s.name = "pling"
|
7
7
|
s.version = Pling::VERSION
|
8
|
-
s.authors = ["
|
8
|
+
s.authors = ["Benedikt Deicke", "Konstantin Tennhard", "Christian Bäuerlein"]
|
9
9
|
s.email = ["benedikt@synatic.net", "me@t6d.de", "fabrik42@gmail.com"]
|
10
10
|
s.homepage = "https://flinc.github.com/pling"
|
11
11
|
s.summary = %q{Pling is a notification framework that supports multiple gateways}
|
@@ -16,9 +16,9 @@ Gem::Specification.new do |s|
|
|
16
16
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
17
|
s.require_paths = ["lib"]
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
|
19
|
+
s.add_runtime_dependency "faraday", "~> 0.7"
|
20
|
+
s.add_runtime_dependency "json", "~> 1.4"
|
21
|
+
s.add_runtime_dependency("jruby-openssl") if RUBY_PLATFORM == 'java'
|
22
22
|
|
23
23
|
s.add_development_dependency "rspec", "~> 2.7"
|
24
24
|
s.add_development_dependency "yard", ">= 0.7"
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Pling::Adapter::Base do
|
4
|
+
|
5
|
+
describe '.deliver' do
|
6
|
+
|
7
|
+
let(:device) { Pling::Device.new }
|
8
|
+
let(:message) { Pling::Message.new }
|
9
|
+
let(:gateway) { mock(:deliver => true) }
|
10
|
+
|
11
|
+
it 'should try to discover a gateway' do
|
12
|
+
Pling::Gateway.should_receive(:discover).with(device).and_return(gateway)
|
13
|
+
subject.deliver(message, device)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should try to deliver to the discoveredgateway' do
|
17
|
+
gateway.should_receive(:deliver).with(message, device)
|
18
|
+
Pling::Gateway.stub(:discover).and_return(gateway)
|
19
|
+
subject.deliver(message, device)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Pling::DelayedInitializer do
|
4
|
+
|
5
|
+
it { should be_kind_of Array }
|
6
|
+
it { should be_empty }
|
7
|
+
|
8
|
+
describe '#initialize!' do
|
9
|
+
it 'should initialize all stored arrays' do
|
10
|
+
subject << [String, "new string"]
|
11
|
+
subject << [String, "other string"]
|
12
|
+
subject.initialize!
|
13
|
+
subject.should == ["new string", "other string"]
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should not change other object instances' do
|
17
|
+
object = Object.new
|
18
|
+
subject << object
|
19
|
+
subject.initialize!
|
20
|
+
subject.first.should be === object
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#use' do
|
25
|
+
it 'should add the arguments as an item to the array' do
|
26
|
+
subject.use String, "new string"
|
27
|
+
subject.should eq([[String, "new string"]])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Pling::Device do
|
4
|
+
|
5
|
+
context 'when created with no arguments' do
|
6
|
+
it 'should not require an argument' do
|
7
|
+
expect { Pling::Device.new }.to_not raise_error ArgumentError
|
8
|
+
end
|
9
|
+
|
10
|
+
specify { Pling::Device.new.should_not be_valid }
|
11
|
+
end
|
12
|
+
|
13
|
+
context 'when created with an empty hash' do
|
14
|
+
it 'should accept a hash of attributes' do
|
15
|
+
expect { Pling::Device.new({}) }.to_not raise_error
|
16
|
+
end
|
17
|
+
|
18
|
+
specify { Pling::Device.new({}).should_not be_valid }
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'when created with an hash of valid attributes' do
|
22
|
+
subject { Pling::Device.new(:identifier => 'XXXX', :type => 'android') }
|
23
|
+
|
24
|
+
its(:identifier) { should eq('XXXX') }
|
25
|
+
its(:type) { should eq(:android) }
|
26
|
+
|
27
|
+
it { should be_valid }
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'when created with an hash of invalid attributes' do
|
31
|
+
it 'should ignore the invalid paramters' do
|
32
|
+
expect { Pling::Device.new({ :random_param => true }) }.to_not raise_error
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#to_pling_device' do
|
37
|
+
it 'should return self' do
|
38
|
+
subject.to_pling_device.should be === subject
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe '#identifier=' do
|
43
|
+
it 'should call #to_s on the given identifier' do
|
44
|
+
subject.identifier = stub(:to_s => 'XXXX')
|
45
|
+
subject.identifier.should eq('XXXX')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '#type=' do
|
50
|
+
it 'should call #to_sym on the given type' do
|
51
|
+
subject.type = 'android'
|
52
|
+
subject.type.should eq(:android)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
it { should respond_to :deliver }
|
57
|
+
|
58
|
+
describe '#deliver' do
|
59
|
+
subject { Pling::Device.new(:identifier => 'XXXX', :type => 'android') }
|
60
|
+
|
61
|
+
let(:message) { Pling::Message.new }
|
62
|
+
let(:gateway) { stub(:deliver => true) }
|
63
|
+
|
64
|
+
before { Pling::Gateway.stub(:discover => gateway) }
|
65
|
+
|
66
|
+
it 'should require a message as parameter' do
|
67
|
+
expect { subject.deliver }.to raise_error ArgumentError
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should deliver the given message to an gateway' do
|
71
|
+
gateway.should_receive(:deliver).with(message, subject)
|
72
|
+
subject.deliver(message)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|