Class: PYAPNS::Client

Inherits:
Object
  • Object
show all
Includes:
Singleton
Defined in:
lib/pyapns.rb

Overview

PYAPNS::Client There’s python in my ruby!

This is a class used to send notifications, provision applications and retrieve feedback using the Apple Push Notification Service.

PYAPNS is a multi-application APS provider, meaning it is possible to send notifications to any number of different applications from the same application and same server. It is also possible to scale the client to any number of processes and servers, simply balanced behind a simple web proxy.

It may seem like overkill for such a bare interface - after all, the APS service is rather simplistic. However, PYAPNS takes no shortcuts when it comes to compleatness/compliance with the APNS protocol and allows the user many optimization and scaling vectors not possible with other libraries. No bandwidth is wasted, connections are persistent and the server is asynchronous therefore notifications are delivered immediately.

PYAPNS takes after the design of 3rd party push notification service that charge a fee each time you push a notification, and charge extra for so-called ‘premium’ service which supposedly gives you quicker access to the APS servers. However, PYAPNS is free, as in beer and offers more scaling oportunites without the financial draw.

Provisioning

To add your app to the PYAPNS server, it must be ‘provisioned` at least once. Normally this is done once upon the start-up of your application, be it a web service, desktop application or whatever… It must be done at least once to the server you’re connecting to. Multiple instances of PYAPNS will have to have their applications provisioned individually. To provision an application manually use the PYAPNS::Client#provision method.

require 'pyapns'
client = PYAPNS::Client.configure
client.provision :app_id => 'cf', :cert => '/home/ss/cert.pem', :env => 'sandbox', :timeout => 15

This basically says “add an app reference named ‘cf’ to the server and start a connection using the certification, and if it can’t within 15 seconds, raise a PYAPNS::TimeoutException

That’s all it takes to get started. Of course, this can be done automatically by using PYAPNS::ClientConfiguration middleware. PYAPNS::Client is a singleton class that is configured using the class method PYAPNS::Client#configure. It is sensibly configured by default, but can be customized by specifying a hash See the docs on PYAPNS::ClientConfiguration for a list of available configuration parameters (some of these are important, and you can specify initial applications) to be configured by default.

Sending Notifications

Once your client is configured, and application provisioned (again, these should be taken care of before you write notification code) you can begin sending notifications to users. If you’re wondering how to acquire a notification token, you’ve come to the wrong place… I recommend using google. However, if you want to send hundreds of millions of notifications to users, here’s how it’s done, one at a time…

The PYAPNS::Client#notify is a sort of polymorphic method which can notify any number of devices at a time. It’s basic form is as follows:

client.notify 'cf', 'long ass app token', {:aps=> {:alert => 'hello?'}}

However, as stated before, it is sort of polymorphic:

client.notify 'cf', ['token', 'token2', 'token3'], [alert, alert2, alert3]

client.notify :app_id => 'cf', :tokens => 'mah token', :notifications => alertHash

client.notify 'cf', 'token', PYAPNS::Notification('hello tits!')

As you can see, the method accepts paralell arrays of tokens and notifications meaning any number of notifications can be sent at once. Hashes will be automatically converted to PYAPNS::Notification objects so they can be optimized for the wire (nil values removed, etc…), and you can pass PYAPNS::Notification objects directly if you wish.

Retrieving Feedback

The APS service offers a feedback functionality that allows application servers to retrieve a list of device tokens it deems to be no longer in use, and the time it thinks they stopped being useful (the user uninstalled your app, better luck next time…) Sounds pretty straight forward, and it is. Apple recommends you do this at least once an hour. PYAPNS will return a list of 2-element lists with the date and the token:

feedbacks = client.feedback 'cf'

Asynchronous Calls

PYAPNS::Client will, by default, perform no funny stuff and operate entirely within the calling thread. This means that certain applications may hang when, say, sending a notification, if only for a fraction of a second. Obviously not a desirable trait, all ‘provision`, `feedback` and `notify` methods also take a block, which indicates to the method you want to call PYAPNS asynchronously, and it will be done so handily in another thread, calling back your block with a single argument when finished. Note that `notify` and `provision` return absolutely nothing (nil, for you rub–wait you are ruby developers!). It is probably wise to always use this form of operation so your calling thread is never blocked (especially important in UI-driven apps and asynchronous servers) Just pass a block to provision/notify/feedback like so:

PYAPNS::Client.instance.feedback do |feedbacks|
   feedbacks.each { |f| trim_token f }
end

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeClient

Returns a new instance of Client.



125
126
127
# File 'lib/pyapns.rb', line 125

def initialize
  @configured = false
end

Class Method Details

.configure(hash = {}) ⇒ Object



120
121
122
123
# File 'lib/pyapns.rb', line 120

def self.configure(hash={})
  y = self.instance
  y.configure(hash)
end

Instance Method Details

#configure(hash = {}) ⇒ Object



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/pyapns.rb', line 202

def configure(hash={})
  if configured?
    return self
  end
  h = {}
  hash.each { |k,v| h[k.to_s.downcase] = v }
  @host = h['host'] || "localhost"
  @port = h['port'] || 7077
  @path = h['path'] || '/'
  @timeout = h['timeout'] || 15
  @client = XMLRPC::Client.new3(
    :host => @host, 
    :port => @port, 
    :timeout => @timeout, 
    :path => @path)
  if not h['initial'].nil?
    h['initial'].each do |initial|
      provision(:app_id => initial[:app_id], 
                :cert => initial[:cert], 
                :env => initial[:env], 
                :timeout => initial[:timeout] || 15)
    end
  end
  @configured = true
  self
end

#configured?Boolean

Returns:

  • (Boolean)


198
199
200
# File 'lib/pyapns.rb', line 198

def configured?
  return @configured
end

#feedback(*args, &block) ⇒ Object



148
149
150
# File 'lib/pyapns.rb', line 148

def feedback(*args, &block)
  perform_call :feedback, args, :app_id, &block
end

#get_args(splat, *args, &block) ⇒ Object



170
171
172
173
174
175
176
177
178
179
# File 'lib/pyapns.rb', line 170

def get_args(splat, *args, &block)
  if splat.length == 1 && splat[0].class == Hash
    splat = args.map { |k| splat[0][k] }
  end
  if (splat.find_all { |l| not l.nil? }).length == args.length
    block.call(splat)
  else
    raise PYAPNS::InvalidArguments.new "Invalid args supplied #{args}"
  end
end

#notify(*args, &block) ⇒ Object



133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/pyapns.rb', line 133

def notify(*args, &block)
  kwargs = [:app_id, :tokens, :notifications]
  get_args(args, *kwargs) do |splat|
    splat[2] = (splat[2].class == Array ? 
                splat[2] : [splat[2]]).map do |note|
                  if note.class != PYAPNS::Notification
                    PYAPNS::Notification.encode note
                  else
                    note
                  end
                end
    perform_call :notify, splat, *kwargs, &block
  end
end

#perform_call(method, splat, *args, &block) ⇒ Object



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/pyapns.rb', line 152

def perform_call(method, splat, *args, &block)
  if !configured?
    raise PYAPNS::NotConfigured.new
  end
  get_args(splat, *args) do |splat|
    if block_given?
      Thread.new do 
        perform_call2 {
          block.call(@client.call_async(method.to_s, *splat)) 
        }
      end
      nil
    else
      perform_call2 { @client.call_async(method.to_s, *splat) }
    end
  end
end

#perform_call2(&block) ⇒ Object



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/pyapns.rb', line 181

def perform_call2(&block)
  begin
    block.call()
  rescue XMLRPC::FaultException => fault
    case fault.faultCode
    when 404
      raise PYAPNS::UnknownAppID.new fault.faultString
    when 401
      raise PYAPNS::InvalidEnvironment.new fault.faultString
    when 500
      raise PYAPNS::ServerTimeout.new fault.faultString
    else
      raise fault
    end
  end
end

#provision(*args, &block) ⇒ Object



129
130
131
# File 'lib/pyapns.rb', line 129

def provision(*args, &block)
  perform_call :provision, args, :app_id, :cert, :env, :timeout, &block
end