File Coverage

blib/lib/Mojo/APNS.pm
Criterion Covered Total %
statement 22 96 22.9
branch 4 38 10.5
condition 0 8 0.0
subroutine 7 17 41.1
pod 2 2 100.0
total 35 161 21.7


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