AnyEvent-APNS

 view release on metacpan or  search on metacpan

lib/AnyEvent/APNS.pm  view on Meta::CPAN

package AnyEvent::APNS;
use utf8;
use Any::Moose;

use AnyEvent 4.80;
use AnyEvent::Handle;
use AnyEvent::Socket;
use AnyEvent::TLS;

require bytes;
use Carp qw(croak);
use Encode;
use Scalar::Util 'looks_like_number';
use JSON::Any;

our $VERSION = '0.10';

has certificate => (
    is       => 'rw',
    isa      => 'Str | ScalarRef',
    required => 1,
);

has private_key => (
    is       => 'rw',
    isa      => 'Str | ScalarRef',
    required => 1,
);

has sandbox => (
    is      => 'rw',
    isa     => 'Bool',
    default => 0,
);

has handler => (
    is        => 'rw',
    isa       => 'AnyEvent::Handle',
    predicate => 'connected',
    clearer   => 'clear_handler',
);

has json_driver => (
    is      => 'rw',
    isa     => 'Object',
    lazy    => 1,
    default => sub {
        JSON::Any->new( utf8 => 1 );
    },
);

has on_error => (
    is      => 'rw',
    isa     => 'CodeRef',
    default => sub { sub { warn @_ } },
);

has on_eof => (
    is  => 'rw',
    isa => 'CodeRef',
);

has on_connect => (
    is      => 'rw',
    isa     => 'CodeRef',
    default => sub { sub {} },
);

has on_error_response => (
    is  => 'rw',
    isa => 'CodeRef',
);

has debug_port => (
    is        => 'rw',
    isa       => 'Int',
    predicate => 'is_debug',
);

has _con_guard => (
    is  => 'rw',
    isa => 'Object',
);

has last_identifier => (
    is      => 'rw',
    isa     => 'Int',
    default => sub { 0; }
);

no Any::Moose;

sub send {
    my $self = shift;
    my ($token, $payload, $expiry) = @_;

    my $json = encode_utf8( $self->json_driver->encode($payload) );

    # http://developer.apple.com/library/ios/#DOCUMENTATION/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html
    # Expiry—A fixed UNIX epoch date expressed in seconds (UTC) that identifies when the notification is no longer valid and can be discarded. The expiry value should be in network order (big endian). If the expiry value is positive, APNs tries to ...
    # default to 24 hours
    $expiry  = defined $expiry ? $expiry : time() + 3600 * 24;

    # Identifier—An arbitrary value that identifies this notification. This same identifier is returned in a error-response packet if APNs cannot interpret a notification.
    my $next_identifier = $self->_increment_identifier;

    my $h = $self->handler;

    $h->push_write( pack('C', 1) ); # command
    $h->push_write( pack('N', $next_identifier) );
    $h->push_write( pack('N', $expiry) );
    $h->push_write( pack('n', bytes::length($token)) ); # token length
    $h->push_write( $token );                           # device token

    # Apple Push Notification Service refuses string values as badge number
    if ($payload->{aps}{badge} && looks_like_number($payload->{aps}{badge})) {
        $payload->{aps}{badge} += 0;
    }

    # The maximum size allowed for a notification payload is 256 bytes;
    # Apple Push Notification Service refuses any notification that exceeds this limit.
    if ( (my $exceeded = bytes::length($json) - 256) > 0 ) {
        if (ref $payload->{aps}{alert} eq 'HASH') {
            $payload->{aps}{alert}{body} =
                $self->_trim_utf8($payload->{aps}{alert}{body}, $exceeded);
        }
        else {
            $payload->{aps}{alert} = $self->_trim_utf8($payload->{aps}{alert}, $exceeded);
        }

        $json = encode_utf8( $self->json_driver->encode($payload) );
    }

    $h->push_write( pack('n', bytes::length($json)) ); # payload length
    $h->push_write( $json );                           # payload

    return $next_identifier;
}

sub _trim_utf8 {
    my ($self, $string, $trim_length) = @_;

    my $string_bytes = encode_utf8($string);
    my $trimmed = '';

    my $start_length = bytes::length($string_bytes) - $trim_length;
    return $trimmed if $start_length <= 0;

    for my $len ( reverse $start_length - 6 .. $start_length ) {
        local $@;
        eval {
            $trimmed = decode_utf8(substr($string_bytes, 0, $len), Encode::FB_CROAK);
        };
        last if $trimmed;
    }

    return $trimmed;
}

sub connect {
    my $self = shift;

    if ($self->connected && $self->handler) {
        warn 'Already connected!';
        return;
    }

    my $host = $self->sandbox
        ? 'gateway.sandbox.push.apple.com'
        : 'gateway.push.apple.com';
    my $port = 2195;

    if ($self->is_debug) {
        $host = '127.0.0.1';
        $port = $self->debug_port;
    }



( run in 1.886 second using v1.01-cache-2.11-cpan-39bf76dae61 )