pling 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|