File Coverage

blib/lib/Mojo/APNS.pm
Criterion Covered Total %
statement 98 104 94.2
branch 28 40 70.0
condition 6 10 60.0
subroutine 20 20 100.0
pod 3 3 100.0
total 155 177 87.5


line stmt bran cond sub pod time code
1             package Mojo::APNS;
2 3     3   294655 use Mojo::Base 'Mojo::EventEmitter';
  3         115503  
  3         17  
3              
4 3     3   3998 use feature 'state';
  3         6  
  3         67  
5 3     3   1116 use Mojo::JSON 'encode_json';
  3         40714  
  3         163  
6 3     3   1463 use Mojo::IOLoop;
  3         360055  
  3         20  
7 3     3   136 use Mojo::Promise;
  3         6  
  3         22  
8              
9 3     3   75 use constant FEEDBACK_RECONNECT_TIMEOUT => 5;
  3         5  
  3         166  
10 3 50   3   14 use constant DEBUG => $ENV{MOJO_APNS_DEBUG} ? 1 : 0;
  3         7  
  3         4148  
11              
12             our $VERSION = '1.01';
13              
14             has key => '';
15             has cert => '';
16             has insecure => 0;
17             has sandbox => 1;
18              
19             has ioloop => sub { Mojo::IOLoop->singleton };
20             has _feedback_port => 2196;
21             has _gateway_port => 2195;
22             has _gateway_address => sub {
23             $_[0]->sandbox ? 'gateway.sandbox.push.apple.com' : 'gateway.push.apple.com';
24             };
25              
26             sub on {
27 5     5 1 2441 my ($self, $event, @args) = @_;
28              
29 5 100 66     22 if ($event eq 'feedback' and !$self->{feedback_id}) {
30 1         5 $self->_connect(feedback => $self->_connected_to_feedback_deamon_cb);
31             }
32              
33 5         283 $self->SUPER::on($event => @args);
34             }
35              
36             sub send {
37 3 100   3 1 36 my $cb = ref $_[-1] eq 'CODE' ? pop : \&_default_handler;
38 3         8 my $self = shift;
39 3   100 3   8 $self->send_p(@_)->finally(sub { $self->$cb(shift || '') });
  3         339  
40 3         191 $self;
41             }
42              
43             sub send_p {
44 3     3 1 25 my ($self, $device_token, $message, %args) = @_;
45 3         15 my $p = Mojo::Promise->new;
46 3         21 my $data = {};
47              
48 3   50     17 $data->{aps} = {alert => $message, badge => int(delete $args{badge} || 0)};
49              
50 3 100       12 if (defined(my $sound = delete $args{sound})) {
51 1 50       4 $data->{aps}{sound} = $sound if length $sound;
52             }
53              
54 3 100       9 if (defined(my $content_available = delete $args{content_available})) {
55 1 50       10 $data->{aps}{'content-available'} = $content_available if length $content_available;
56             }
57              
58 3 100       12 $data->{custom} = \%args if %args;
59 3         9 $message = encode_json $data;
60              
61 3 100       328 if (length $message > 256) {
62 1         2 my $length = length $message;
63 1         5 $p->reject("Too long message ($length)");
64 1         120 return $p;
65             }
66              
67 2         14 $device_token =~ s/\s//g;
68 2         3 warn "[APNS:$device_token] <<< $message\n" if DEBUG;
69              
70 2     2   28 $self->once(drain => sub { $p->resolve });
  2         142  
71 2         32 $self->_write([chr(0), pack('n', 32), pack('H*', $device_token), pack('n', length $message), $message]);
72              
73 2         12 return $p;
74             }
75              
76             sub _connect {
77 2     2   8 my ($self, $type, $cb) = @_;
78 2 100       10 my $port = $type eq 'gateway' ? $self->_gateway_port : $self->_feedback_port;
79              
80 2         11 if (DEBUG) {
81             my $key = join ':', $self->_gateway_address, $port;
82             warn "[APNS:$key] <<< cert=@{[$self->cert]}\n" if DEBUG;
83             warn "[APNS:$key] <<< key=@{[$self->key]}\n" if DEBUG;
84             }
85              
86 2         9 Scalar::Util::weaken($self);
87             $self->{"${type}_stream_id"} ||= $self->ioloop->client(
88             address => $self->_gateway_address,
89             port => $port,
90             tls => 1,
91             tls_cert => $self->cert,
92             tls_key => $self->key,
93             $self->insecure ? (tls_verify => 0x00) : (),
94             sub {
95 2     2   17734 my ($ioloop, $err, $stream) = @_;
96              
97 2 50       8 $err and return $self->emit(error => "$type: $err");
98 2         13 $stream->on(close => sub { delete $self->{"${type}_stream_id"} });
  0         0  
99 2         21 $stream->on(error => sub { $self->emit(error => "$type: $_[1]") });
  0         0  
100 2         17 $stream->on(drain => sub { $self->emit('drain'); });
  3         1114  
101 2         17 $stream->on(timeout => sub { delete $self->{"${type}_stream_id"} });
  0         0  
102 2         11 $self->$cb($stream);
103             },
104 2 50 33     33 );
105             }
106              
107             sub _connected_to_feedback_deamon_cb {
108 1     1   2 my $self = shift;
109 1         11 my ($bytes, $ts, $device) = ('');
110              
111             sub {
112 1     1   2 my ($self, $stream) = @_;
113 1         4 Scalar::Util::weaken($self);
114 1         3 $stream->timeout(0);
115             $stream->on(
116             close => sub {
117             $stream->reactor->timer(
118             FEEDBACK_RECONNECT_TIMEOUT,
119             sub {
120 0 0       0 $self or return;
121 0         0 $self->_connect(feedback => $self->_connected_to_feedback_deamon_cb);
122             }
123 0         0 );
124             }
125 1         22 );
126             $stream->on(
127             read => sub {
128 1         403 $bytes .= $_[1];
129 1         13 ($ts, $device, $bytes) = unpack 'N n/a a*', $bytes;
130 1         1 warn "[APNS:$device] >>> $ts\n" if DEBUG;
131 1         8 $self->emit(feedback => {ts => $ts, device => $device});
132             }
133 1         8 );
134 1         9 };
135             }
136              
137             sub _default_handler {
138 2 50   2   8 $_[0]->emit(error => $_[1]) if $_[1];
139             }
140              
141             sub _write {
142 4     4   8 my ($self, $message) = @_;
143 4         14 my $id = $self->{gateway_stream_id};
144 4         6 my $stream;
145              
146 4 100       14 unless ($id) {
147 1         2 push @{$self->{messages}}, $message;
  1         5  
148             $self->_connect(
149             gateway => sub {
150 1     1   6 my $self = shift;
151 1 50       1 $self->_write($_) for @{delete($self->{messages}) || []};
  1         7  
152             }
153 1         9 );
154 1         202 return $self;
155             }
156 3 100       8 unless ($stream = $self->ioloop->stream($id)) {
157 1         17 push @{$self->{messages}}, $message;
  1         2  
158 1         2 return $self;
159             }
160              
161 2         44 $stream->write(join '', @$message);
162 2         64 $self;
163             }
164              
165             sub DESTROY {
166 2     2   2729 my $self = shift;
167 2 50       6 my $ioloop = $self->ioloop or return;
168 2         24 my $id;
169              
170 2 50       6 $ioloop->remove($id) if $id = $self->{gateway_id};
171 2 50       272 $ioloop->remove($id) if $id = $self->{feedback_id};
172             }
173              
174             1;
175              
176             =encoding utf8
177              
178             =head1 NAME
179              
180             Mojo::APNS - Apple Push Notification Service for Mojolicious
181              
182             =head1 VERSION
183              
184             1.01
185              
186             =head1 DESCRIPTION
187              
188             This module provides an API for sending messages to an iPhone using Apple Push
189             Notification Service.
190              
191             This module does not support password protected SSL keys.
192              
193             NOTE! This module will segfault if you swap L and L around.
194              
195             =head1 SYNOPSIS
196              
197             =head2 Script
198              
199             use Mojo::Base -strict;
200             use Mojo::APNS;
201              
202             my $device_id = shift @ARGV;
203             my $apns = Mojo::APNS->new(
204             cert => "/path/to/apns-dev-cert.pem",
205             key => "/path/to/apns-dev-key.pem",
206             sandbox => 0,
207             );
208              
209             # Emulate a blocking request with Mojo::IOLoop->start() and stop()
210             $apns->send($device_id, "Hey there!", sub { shift->ioloop->stop })->ioloop->start;
211              
212             =head2 Web application
213              
214             use Mojolicious::Lite;
215             use Mojo::APNS;
216              
217             # set up a helper that holds the Mojo::APNS object
218             helper apns => sub {
219             state $apns
220             = Mojo::APNS->new(
221             cert => "/path/to/apns-dev-cert.pem",
222             key => "/path/to/apns-dev-key.pem",
223             sandbox => 0,
224             );
225             };
226              
227             # send a notification
228             post "/notify" => sub {
229             my $c = shift;
230             my $device_id = "c9d4a07c fbbc21d6 ef87a47d 53e16983 1096a5d5 faa15b75 56f59ddd a715dff4";
231              
232             $c->apns
233             ->send_p($device_id, "hey there!", $delay->begin)
234             ->then(
235             sub { $c->render(text => "Message was sent!") },
236             sub { $c->reply->exception(shift); }
237             );
238             };
239              
240             # listen for feedback events
241             app->apns->on(
242             feedback => sub {
243             my ($apns, $feedback) = @_;
244             warn "$feedback->{device} rejected push at $feedback->{ts}";
245             }
246             );
247              
248             app->start;
249              
250             =head1 EVENTS
251              
252             =head2 error
253              
254             $self->on(error => sub { my ($self, $err) = @_; });
255              
256             Emitted when an error occurs between client and server.
257              
258             =head2 drain
259              
260             $self->on(drain => sub { my ($self) = @_; });
261              
262             Emitted once all messages have been sent to the server.
263              
264             =head2 feedback
265              
266             $self->on(feedback => sub { my ($self, $data) = @_; });
267              
268             This event is emitted once a device has rejected a notification. C<$data> is a
269             hash-ref:
270              
271             {
272             ts => $rejected_epoch_timestamp,
273             device => $device_token,
274             }
275              
276             Once you start listening to "feedback" events, a connection will be made to
277             Apple's push notification server which will then send data to this callback.
278              
279             =head1 ATTRIBUTES
280              
281             =head2 cert
282              
283             $self = $self->cert("/path/to/apns-dev-cert.pem");
284             $path = $self->cert;
285              
286             Path to apple SSL certificate.
287              
288             =head2 key
289              
290             $self = $self->key("/path/to/apns-dev-key.pem");
291             $path = $self->key;
292              
293             Path to apple SSL key.
294              
295             =head2 insecure
296              
297             $self = $self->insecure(1);
298             $bool = $self->insecure;
299              
300             Used if you want to send messages to an test server with an insecure TLS
301             sertificate.
302              
303             =head2 sandbox
304              
305             $self = $self->sandbox(0);
306             $bool = $self->sandbox;
307              
308             Boolean true for talking with "gateway.sandbox.push.apple.com" instead of
309             "gateway.push.apple.com". Default is true.
310              
311             =head2 ioloop
312              
313             $self = $self->ioloop(Mojo::IOLoop->new);
314             $ioloop = $self->ioloop;
315              
316             Holds a L object.
317              
318             =head1 METHODS
319              
320             =head2 on
321              
322             Same as L, but will also set up feedback connection if
323             the event is L.
324              
325             =head2 send
326              
327             $self->send($device, $message, %args);
328             $self->send($device, $message, %args, sub { my ($self, $err) = @_; });
329              
330             Will send a C<$message> to the C<$device>. C<%args> is optional, but can contain:
331              
332             C<$cb> will be called when the messsage has been sent or if it could not be
333             sent. C<$err> will be false on success.
334              
335             =over 4
336              
337             =item * badge
338              
339             The number placed on the app icon. Default is 0.
340              
341             =item * sound
342              
343             Default is "default".
344              
345             =item * Custom arguments
346              
347             =back
348              
349             =head2 send_p
350              
351             $promise = $self->send_p($device, $message, %args);
352              
353             L takes the same arguments as L, but returns a L.
354              
355             =head1 AUTHOR
356              
357             Glen Hinkle - C
358              
359             Jan Henning Thorsen - C
360              
361             =cut