Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

First commit. Drafted README. Spiked implementation of Delivery class.

  • Loading branch information...
commit b90dd90c545a840640736d9b16b71d91f85450d7 0 parents
@peter authored
4 .gitignore
@@ -0,0 +1,4 @@
+*.gem
+.bundle
+Gemfile.lock
+pkg/*
2  .rspec
@@ -0,0 +1,2 @@
+--color
+--format documentation
47 .rvmrc
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+
+# This is an RVM Project .rvmrc file, used to automatically load the ruby
+# development environment upon cd'ing into the directory
+
+# First we specify our desired <ruby>[@<gemset>], the @gemset name is optional.
+environment_id="ruby-1.9.2-p290@apn_client"
+
+#
+# Uncomment following line if you want options to be set only for given project.
+#
+# PROJECT_JRUBY_OPTS=( --1.9 )
+
+#
+# First we attempt to load the desired environment directly from the environment
+# file. This is very fast and efficient compared to running through the entire
+# CLI and selector. If you want feedback on which environment was used then
+# insert the word 'use' after --create as this triggers verbose mode.
+#
+if [[ -d "${rvm_path:-$HOME/.rvm}/environments" \
+ && -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]]
+then
+ \. "${rvm_path:-$HOME/.rvm}/environments/$environment_id"
+
+ if [[ -s "${rvm_path:-$HOME/.rvm}/hooks/after_use" ]]
+ then
+ . "${rvm_path:-$HOME/.rvm}/hooks/after_use"
+ fi
+else
+ # If the environment file has not yet been created, use the RVM CLI to select.
+ if ! rvm --create "$environment_id"
+ then
+ echo "Failed to create RVM environment '${environment_id}'."
+ exit 1
+ fi
+fi
+
+#
+# If you use an RVM gemset file to install a list of gems (*.gems), you can have
+# it be automatically loaded. Uncomment the following and adjust the filename if
+# necessary.
+#
+# filename=".gems"
+# if [[ -s "$filename" ]] ; then
+# rvm gemset import "$filename" | grep -v already | grep -v listed | grep -v complete | sed '/^$/d'
+# fi
+
4 Gemfile
@@ -0,0 +1,4 @@
+source "http://rubygems.org"
+
+# Specify your gem's dependencies in apn_client.gemspec
+gemspec
84 README.md
@@ -0,0 +1,84 @@
+# APN BroadCast RubyGem
+
+## Introduction
+
+This is a RubyGem that allows sending of Apple Push Notifications to iOS devices (i.e. iPhones, iPads) from Ruby. The main features are:
+
+* Broadcasting of notifications to a large number of devices in a reliable fashion
+* Dealing with errors (via the enhanced format Apple protocol) when sending notifications
+* Reading from the Apple Feedback Service to avoid sending to devices with uninstalled applications
+
+## Usage
+
+### 1. Configure the Connection
+
+```
+
+ApnClient::Delivery.connection_config = {
+ :host => 'gateway.push.apple.com', # For sandbox, use: gateway.sandbox.push.apple.com
+ :port => 2195,
+ :certificate => IO.read("my_apn_certificate.pem"),
+ :certificate_passphrase => '',
+}
+
+ApnClient::Feedback.connection_config = {
+ :host => 'feedback.push.apple.com', # For sandbox, use: feedback.sandbox.push.apple.com
+ :port => 2196,
+ :certificate => IO.read("my_apn_certificate.pem"),
+ :certificate_passphrase => '',
+}
+
+```
+
+### 2. Deliver Your Message
+
+```
+message1 = ApnClient::Message.new(
+ :device_token => "7b7b8de5888bb742ba744a2a5c8e52c6481d1deeecc283e830533b7c6bf1d099",
+ :alert => "New version of the app is out. Get it now in the app store!",
+ :badge_count => 2
+)
+message1 = ApnClient::Message.new(
+ :device_token => "6a5g4de5888bb742ba744a2a5c8e52c6481d1deeecc283e830533b7c6bf1d044",
+ :alert => "New version of the app is out. Get it now in the app store!",
+ :badge_count => 1
+)
+delivery = ApnClient::Delivery.new([message1, message2],
+ :callbacks => {
+ :on_write => lambda { |d, m| puts "Wrote message #{m}" },
+ :on_exception => lambda { |d, m, e| puts "Exception #{e} raised when delivering message #{m}" },
+ :on_failure => lambda { |d, m| puts "Failed/skipping message #{m}" },
+ :on_error => lambda { |d, message_id, error_code| puts "Received error code #{error_code} from Apple for message #{message_id}" }
+ },
+ :consecutive_failure_limit => 10, # If more than 10 devices in a row fail, we abort the whole delivery
+ :exception_limit => 3 # If a device raises an exception three times in a row we fail/skip the device and move on
+)
+delivery.process!
+puts "Delivered successfully to #{delivery.success_count} out of #{delivery.total_count} devices in #{delivery.elapsed} seconds"
+```
+
+### 3. Check for Feedback
+
+TODO
+
+## Dependencies
+
+The payload of an APN message is a JSON formated hash (containing alert message, badge count, content available etc.) and therefore a JSON library needs to be present. This gem requires a Hash#to_json method to be defined (hashes need to respond
+to to_json and return valid JSON). If you for example have the json gem or the rails gem in your environment then this requirement is fulfilled.
+
+The gem is tested on MRI 1.9.2.
+
+## Credits
+
+This gem is an extraction of production code at [Mag+](http://www.magplus.com) and both [Dennis Rogenius](https://github.com/denro) and [Lennart Friden](https://github.com/DevL) made important contributions along the way.
+
+The APN connection code has its origins in the [APN on Rails](https://github.com/jwang/apn_on_rails) gem.
+
+## License
+
+This library is released under the MIT license.
+
+## Resources
+
+* [Apple Push Notifications Documentation](http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008194-CH1-SW1)
+* [The APNS RubyGem](https://github.com/jpoz/APNS). Has a small codebase and a nice API. Does not use the enhanced format protocol and lacks error handling.
5 Rakefile
@@ -0,0 +1,5 @@
+require "bundler/gem_tasks"
+
+require 'rspec/core/rake_task'
+RSpec::Core::RakeTask.new(:spec)
+task :default => :spec
24 apn_client.gemspec
@@ -0,0 +1,24 @@
+# -*- encoding: utf-8 -*-
+$:.push File.expand_path("../lib", __FILE__)
+require "apn_client/version"
+
+Gem::Specification.new do |s|
+ s.name = "apn_client"
+ s.version = ApnClient::VERSION
+ s.authors = ["Peter Marklund"]
+ s.email = ["peter@marklunds.com"]
+ s.homepage = ""
+ s.summary = %q{Library for sending Apple Push Notifications to iOS devices from Ruby}
+ s.description = %q{Uses the "enhanced format" Apple protocol and deals with errors and failures when broadcasting to many devices. Includes support for talking to the Apple Push Notification Feedback service for dealing with uninstalled apps.}
+
+ s.rubyforge_project = "apn_client"
+
+ s.files = `git ls-files`.split("\n")
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+ s.require_paths = ["lib"]
+
+ s.add_development_dependency "rspec"
+ s.add_development_dependency "mocha"
+ # s.add_runtime_dependency "rest-client"
+end
5 lib/apn_client.rb
@@ -0,0 +1,5 @@
+require "apn_client/version"
+
+module ApnClient
+ # Your code goes here...
+end
7 lib/apn_client/connection.rb
@@ -0,0 +1,7 @@
+module ApnClient
+ class Connection
+ def self.valid_config_keys
+ [:host, :port, :certificate, :certificate_passphrase]
+ end
+ end
+end
138 lib/apn_client/delivery.rb
@@ -0,0 +1,138 @@
+module ApnClient
+ class Delivery
+ attr_accessor :message_queue, :callbacks, :consecutive_failure_limit, :exception_limit, :sleep_on_exception,
+ :exception_count, :success_count, :failure_count, :consecutive_failure_count,
+ :started_at, :finished_at
+
+ def initialize(messages, options = {})
+ initialize_message_queue(messages)
+ initialize_options(options)
+ self.exception_count = 0
+ self.success_count = 0
+ self.failure_count = 0
+ end
+
+ def process!
+ self.started_at = Time.now
+ while current_message && consecutive_failure_count < consecutive_failure_limit
+ process_one_message!
+ end
+ close_connection
+ self.finished_at = Time.now
+ end
+
+ def elapsed
+ finished_at ? (finished_at - started_at) : (Time.now - started_at)
+ end
+
+ private
+
+ def initialize_message_queue(messages)
+ if messages.respond_to?(:next)
+ self.message_queue = messages
+ else
+ self.message_queue = messages.to_enum
+ end
+ end
+
+ def initialize_options(options)
+ check_option_keys_valid!(options)
+ check_callback_keys_valid!(options[:callbacks])
+ self.callbacks = options[:callbacks]
+ self.consecutive_failures_limit = options[:consecutive_failures_limit]
+ self.exception_limit = options[:exception_limit]
+ self.sleep_on_exception = options[:sleep_on_exception] || 1
+ end
+
+ def current_message
+ return @current_message if @current_message
+ next_message!
+ end
+
+ def next_message!
+ @current_message = message_queue.next
+ rescue StopIteration
+ nil
+ end
+
+ def process_one_message!
+ begin
+ write_message!
+ check_message_error!
+ rescue Exception => e
+ handle_exception!(e)
+ check_message_error! unless @checked_message_error
+ close_connection
+ end
+ end
+
+ def connection
+ @connection ||= Connection.new(self.class.connection_config)
+ end
+
+ def close_connection
+ @connection.close if @connection
+ @connection = nil
+ end
+
+ def write_message!
+ @checked_message_error = false
+ connection.write(current_message)
+ self.exception_count = 0; self.consecutive_failure_count = 0; self.success_count += 1
+ invoke_callback(:on_write, current_message)
+ next_message!
+ end
+
+ def check_message_error!
+ @checked_message_error = true
+ failed_message_id, error_code = read_apns_error
+ # NOTE: According to the APN documentation the APN service will return an error code prior to
+ # disconnecting. If we don't disconnect here we will attempt to write more messages
+ # before a broken pipe error is raised and those messages will never be delivered.
+ if failed_message_id
+ invoke_callback(:on_error, failed_message_id, error_code)
+ self.failure_count += 1
+ self.success_count -= 1
+ close_connection
+ end
+ end
+
+ def read_apns_error
+ message_id = error_code = nil
+ begin
+ select_return = nil
+ if connection && select_return = connection.select
+ response = connection.read(6)
+ command, status_code, message_id = response.unpack('cci') if response
+ else
+ invoke_callback(:on_nil_select)
+ end
+ rescue Exception => e
+ # NOTE: If we don't catch this exception then one socket read exception could break out of the whole delivery loop
+ invoke_callback(:on_read_exception, e)
+ end
+ return message_id, error_code
+ end
+
+ def handle_exception!(e)
+ self.exception_count += 1
+ fail_message! if exception_limit_reached?
+ sleep(sleep_on_exception) if sleep_on_exception
+ end
+
+ def exception_limit_reached?
+ exception_count == exception_limit
+ end
+
+ # # Give up on the message and move on to the next one
+ def fail_message!
+ self.failure_count += 1; self.consecutive_failures += 1; self.exception_count = 0
+ invoke_callback(:on_failure, current_message)
+ next_message!
+ end
+
+ def invoke_callback(name, *args)
+ callbacks[name].call(self, *args) if callbacks[name]
+ end
+ end
+end
4 lib/apn_client/message.rb
@@ -0,0 +1,4 @@
+module ApnClient
+ class Message
+ end
+end
3  lib/apn_client/version.rb
@@ -0,0 +1,3 @@
+module ApnClient
+ VERSION = "0.0.1"
+end
17 spec/connection_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+require 'apn_client/connection'
+
+describe ApnClient::Connection do
+ describe "#initialize" do
+ it "accepts a config hash"
+ it "allows the config hash to have string keys"
+ it "does not accept a config hash with invalid keys"
+ end
+
+ describe ".valid_config_keys" do
+ it "returns a list of keys that are allowed to be in the config hash" do
+ ApnClient::Connection.valid_config_keys.should == [:host, :port, :certificate, :certificate_passphrase]
+ end
+ end
+end
3  spec/spec_helper.rb
@@ -0,0 +1,3 @@
+RSpec.configure do |config|
+ config.mock_with :mocha
+end
Please sign in to comment.
Something went wrong with that request. Please try again.