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.
@@ -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
@@ -0,0 +1,5 @@
1
+ module Pling
2
+ module Middleware
3
+ autoload :Base, 'pling/middleware/base'
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ module Pling
2
+ module Middleware
3
+ class Base
4
+ include Pling::Configurable
5
+
6
+ def initialize(configuration = {})
7
+ setup_configuration(configuration)
8
+ end
9
+
10
+ def deliver(message, device)
11
+ yield(message, device)
12
+ end
13
+ end
14
+ end
15
+ end
data/lib/pling/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pling
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
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 = ["benedikt", "t6d", "fabrik42"]
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
- # specify any dependencies here; for example:
20
- # s.add_development_dependency "rspec"
21
- # s.add_runtime_dependency "rest-client"
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