pling 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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