File Coverage

blib/lib/WebService/Pushwoosh.pm
Criterion Covered Total %
statement 24 89 26.9
branch 0 12 0.0
condition 0 8 0.0
subroutine 8 24 33.3
pod 9 9 100.0
total 41 142 28.8


line stmt bran cond sub pod time code
1             package WebService::Pushwoosh;
2              
3 2     2   47549 use 5.006;
  2         9  
  2         71  
4 2     2   12 use strict;
  2         2  
  2         59  
5 2     2   9 use warnings;
  2         14  
  2         117  
6              
7             =head1 NAME
8              
9             WebService::Pushwoosh - An interface to the Pushwoosh Remote API v1.3
10              
11             =head1 SYNOPSIS
12              
13             # Create a WebService::Pushwoosh instance
14             my $pw = WebService::Pushwoosh->new(
15             app_code => '00000-00000',
16             api_token => 'YOUR_APP_TOKEN'
17             );
18            
19             # Send a message to all your app's subscribers
20             $pw->create_message(content => "Hello, world!");
21            
22             # Limit to one device
23             $pw->create_message(
24             content => 'Pssst',
25             devices =>
26             ['dec301908b9ba8df85e57a58e40f96f523f4c2068674f5fe2ba25cdc250a2a41']
27             );
28              
29             See below for further examples.
30              
31             =head1 DESCRIPTION
32              
33             L is a push notification service which
34             provides a JSON API for users of its premium account. This module provides a
35             simple Perl wrapper around that API.
36              
37             For information on integrating the Pushwoosh service into your mobile apps, see
38             L.
39              
40             To obtain an API token, log in to your Pushwoosh account and visit
41             L.
42              
43             =head1 VERSION
44              
45             Version 0.02
46              
47             =head1 CONSTRUCTOR
48              
49             =head2 new
50              
51             my $pw = WebService::Pushwoosh->new(
52             app_code => '00000-00000',
53             api_token => 'YOUR_APP_TOKEN'
54             );
55              
56             Creates a WebService::Pushwoosh instance.
57              
58             Parameters:
59              
60             =over
61              
62             =item app_code
63              
64             Your Pushwoosh application code (required)
65              
66             =item api_token
67              
68             Your API token from Pushwoosh (required)
69              
70             =item api_url
71              
72             The API url for Pushwoosh (optional).
73              
74             It is not recommended to change this from the default.
75              
76             =item furl
77              
78             A custom L object to use for the requests (optional).
79              
80             It is not recommended to change this.
81              
82             =item error_mode
83              
84             Set this to either C<'croak'> or C<'manual'>. C<'croak'> is the default and
85             will generate an error string if an error is detected from in the Pushwoosh
86             response. C<'manual'> will simply return the API status response when a method
87             errors, if you want more control over the error handling. See the Pushwoosh
88             documentation for the possible error codes.
89              
90             =back
91              
92             =cut
93              
94             our $VERSION = '0.02';
95              
96 2     2   12 use Carp;
  2         3  
  2         175  
97 2     2   1732 use Furl;
  2         181298  
  2         78  
98 2     2   2568 use JSON qw(from_json to_json);
  2         31872  
  2         13  
99 2     2   57464 use Params::Validate qw(validate validate_with validate_pos :types);
  2         72219  
  2         476  
100 2     2   19 use Try::Tiny;
  2         4  
  2         3128  
101              
102             sub new {
103 0     0 1   my $class = shift;
104 0           my %args = validate(
105             @_,
106             { app_code => 1,
107             api_token => 1,
108             api_url => { default => 'https://cp.pushwoosh.com/json/1.3' },
109             furl => 0,
110             error_mode => { default => 'croak' },
111             }
112             );
113 0   0       $args{furl} ||= Furl->new;
114 0           my $self = bless {%args}, $class;
115 0           return $self;
116             }
117              
118             =head1 METHODS
119              
120             =cut
121              
122             my %Errors = (
123             createMessage => {
124             200 => { 210 => 'Argument error' },
125             400 => { 'n/a' => 'Malformed request string' },
126             500 => { 500 => 'Internal error' }
127             },
128             );
129              
130             $Errors{$_} = $Errors{createMessage}
131             for (qw(deleteMessage registerDevice unregisterDevice));
132              
133             my %Message_spec = (
134              
135             # Content params
136             send_date => { default => 'now', type => SCALAR },
137             page_id => { type => SCALAR, optional => 1 },
138             link => { type => SCALAR, optional => 1 },
139             data => { type => HASHREF, optional => 1 },
140             platforms => { type => ARRAYREF, optional => 1 }
141             , # 1 - iOS; 2 - BB; 3 - Android; 4 - Nokia; 5 - Windows Phone; 7 - OS X
142             # Windows Phone 7 params
143             wp_type => { type => SCALAR, optional => 1 },
144             wp_backbackground => { type => SCALAR, optional => 1 },
145             wp_background => { type => SCALAR, optional => 1 },
146             wp_backtitle => { type => SCALAR, optional => 1 },
147             wp_count => { type => SCALAR, optional => 1 },
148              
149             # Android
150             android_icon => { type => SCALAR, optional => 1 },
151             android_custom_icon => { type => SCALAR, optional => 1 },
152             android_banner => { type => SCALAR, optional => 1 },
153             android_root_params => { type => HASHREF, optional => 1 },
154             android_sound => { type => SCALAR, optional => 1 },
155              
156             # iOS
157             ios_badges => { type => SCALAR, optional => 1 },
158             ios_root_params => { type => HASHREF, optional => 1 },
159             ios_sound => { type => SCALAR, optional => 1 },
160              
161             # Recipients
162             conditions => { type => ARRAYREF, optional => 1 },
163             devices => { type => ARRAYREF, optional => 1 },
164             filter => { type => SCALAR, optional => 1 },
165             );
166              
167             my %Notify_all_spec = (%Message_spec, message => { type => SCALAR },);
168              
169             my %Notification_spec
170             = (%Message_spec, content => { type => SCALAR | HASHREF },);
171              
172             sub _auth_and_app {
173 0     0     my $self = shift;
174 0           return ($self->_app, $self->_auth);
175             }
176              
177             sub _auth {
178 0     0     my $self = shift;
179 0           return (auth => $self->{api_token});
180             }
181              
182             sub _app {
183 0     0     my $self = shift;
184 0           return (application => $self->{app_code});
185             }
186              
187             =head2 create_message
188              
189             my $message_id = $pw->create_message(
190             # Content settings
191             "send_date" => "now", # YYYY-MM-DD HH => mm OR 'now'
192             "content" =>
193             { # Object( language1 => 'content1', language2 => 'content2' ) OR string
194             "en" => "English",
195             "de" => "Deutsch"
196             },
197             "page_id" => 39, # Optional. int
198             "link" => "http://google.com", # Optional. string
199             "data" =>
200             { # HashRef. Will be passed as "u" parameter in the payload
201             'foo' => 1,
202             'favo_bludd' => 'axlotl_tanks',
203             'tleilaxu_master' => 'glossu_rabban',
204             },
205             "platforms" => [1, 2, 3, 4, 5, 6, 7], # 1 - iOS; 2 - BB; 3 - Android; 4 - Nokia; 5 - Windows Phone; 7 - OS X
206              
207             # WP7 related
208             "wp_type" => "Tile", # WP7 notification type. 'Tile' or 'Toast'. Raw notifications are not supported. 'Tile' is default
209             "wp_background" => "/Resources/Red.jpg", # WP7 Tile image
210             "wp_backbackground" => "/Resources/Green.jpg", # WP7 Back tile image
211             "wp_backtitle" => "back title", # WP7 Back tile title
212             "wp_count" => 3, # Optional. Integer. Badge for WP7
213              
214             # Android related
215             "android_banner" => "http://example.com/banner.png",
216             "android_custom_icon" => "http://example.com/image.png",
217             "android_icon" => "icon.png",
218             "android_root_params" => { "key" => "value" }, # custom key-value object. root level parameters for the android payload
219             "android_sound" => "soundfile", # Optional. Sound file name in the "res/raw" folder, do not include the extension
220              
221             #iOS related
222             "ios_badges" => 5, # Optional. Integer. This value will be sent to ALL devices given in "devices"
223             "ios_sound" => "soundfile", # Optional. Sound file name in the main bundle of application
224             "ios_root_params" => { "content-available" => 1 }, # Optional - root level parameters to the aps dictionary
225            
226             # Mac related
227             "mac_badges" => 3,
228             "mac_sound" => "sound.caf",
229             "mac_root_params" => { "content-available" => 1 },
230              
231             # Recipients
232             "devices" =>
233             [ # Optional. If set, message will only be delivered to the devices in the list. Ignored if the applications group is used
234             "dec301908b9ba8df85e57a58e40f96f523f4c2068674f5fe2ba25cdc250a2a41"
235             ],
236             "filter" => "FILTER_NAME" # Optional
237             "conditions" => [TAG_CONDITION1, TAG_CONDITION2, ..., TAG_CONDITIONN] # Optional
238             );
239              
240             Sends a push notification using the C API call. Croaks on errors.
241              
242             Parameters:
243              
244             =over
245              
246             =item content
247              
248             The message text to be delivered to the application
249              
250             =item data
251              
252             Use only to pass custom data to the application. B that iOS push
253             is limited to 256 bytes
254              
255             =item page_id
256              
257             HTML page id (created from Application's HTML Pages). Use this if you want to
258             deliver additional HTML content
259              
260             =item send_date
261              
262             The time at which the message should be sent (UTC) or 'now' to send immediately
263             (the default)
264              
265             =item wp_count
266              
267             Sets the badge for the WP7 platform
268              
269             =item ios_badges
270              
271             Sets the badge on the icon for iOS
272              
273             =item devices (ArrayRef)
274              
275             Limit only to the specified device IDs
276              
277             =item ios_root_params
278              
279             Root level parameters to the aps direction, for example to use with NewsStand
280             apps
281              
282             =item conditions
283              
284             TAG_CONDITION is an array like: [tagName, operator, operand] where
285              
286             =over 8
287              
288             =item * C String
289              
290             =item * C "LTE"|"GTE"|"EQ"|"BETWEEN"|"IN"
291              
292             =item * C String|Integer|ArrayRef
293              
294             =back
295              
296             Valid operators for String tags:
297              
298             =over 8
299              
300             =item * EQ: tag value equals operand. Operand must be a string
301              
302             =back
303              
304             Valid operators for Integer tags:
305              
306             =over 8
307              
308             =item * GTE: tag value greater than or equal to operand. Operand must be an integer.
309              
310             =item * LTE: tag value less than or equal to operand. Operand must be an integer.
311              
312             =item * EQ: tag value equals operand. Operand must be an integer.
313              
314             =item * BETWEEN: tag value greater than or equal to min value, and tag value is less than or equal to max value. Operand must be an array like: C<[min_value, max_value]>.
315              
316             =back
317              
318             Valid operators for ArrayRef tags:
319              
320             =over 8
321              
322             =item * IN: Intersect user values and operand. Operand must be an arrayref of strings like: ["value 1", "value 2", "value N"].
323              
324             =back
325              
326             B
327              
328             =back
329              
330             Returns:
331              
332             =over
333              
334             =item The message ID
335              
336             =back
337              
338             =cut
339              
340             sub create_message {
341 0     0 1   my $self = shift;
342 0           my %args = validate_with(
343             params => \@_,
344             spec => \%Notification_spec,
345             );
346 0           my %options = ($self->_auth_and_app, notifications => [\%args]);
347 0           my $res = $self->_post_data(data => \%options, api_method => 'createMessage');
348 0 0         return $res if !$res->{response}{Messages}[0];
349 0           return $res->{response}{Messages}[0];
350             }
351              
352             =head2 delete_message
353              
354             $pw->delete_message(message => '78EA-F351D565-9CCA7EED');
355              
356             Deletes a scheduled message
357              
358             Parameters:
359              
360             =over
361              
362             =item message
363              
364             The message code obtained from create_message
365              
366             =back
367              
368             =cut
369              
370             sub delete_message {
371 0     0 1   my $self = shift;
372 0           my %args = validate(@_, { message => { type => SCALAR }, });
373 0           my %options = ($self->_auth, %args);
374 0           $self->_post_data(data => \%options, api_method => 'deleteMessage');
375             }
376              
377             =head2 register_device
378              
379             $pw->register_device(
380             application => 'APPLICATION_CODE',
381             push_token => 'DEVICE_PUSH_TOKEN',
382             language => 'en', # optional
383             hwid => 'hardware id',
384             timezone => 3600, # offset in seconds
385             device_type => 1,
386             );
387              
388             Registers device for the application
389              
390             Parameters:
391              
392             =over
393              
394             =item push_token
395              
396             Push token for the device
397              
398             =item language
399              
400             Language locale of the device (optional)
401              
402             =item hwid
403              
404             Unique string to identify the device (Please note that accessing UDID on iOS is
405             deprecated and not allowed, one of the alternative ways now is to use MAC
406             address)
407              
408             =item timezone
409              
410             Timezone offset in seconds for the device (optional)
411              
412             =item device_type
413              
414             1 - iphone, 2 - blackberry, 3 - android, 4 - nokia, 5 - WP7, 7 - mac
415              
416             =back
417              
418             =cut
419              
420             sub register_device {
421 0     0 1   my $self = shift;
422 0           my %args = validate(
423             @_,
424             { push_token => { type => SCALAR },
425             hwid => { type => SCALAR },
426             language => { type => SCALAR, optional => 1 },
427             timezone => { type => SCALAR, optional => 1 },
428             device_type => { type => SCALAR },
429             }
430             );
431 0           my %options = ($self->_app, %args);
432 0           $self->_post_data(data => \%options, api_method => 'registerDevice');
433             }
434              
435             =head2 unregister_device
436              
437             $pw->unregister_device(hwid => 'hardware device id');
438              
439             Remove device from the application
440              
441             Parameters:
442              
443             =over
444              
445             =item hwid
446              
447             Hardware device id used in L function call
448              
449             =back
450              
451             =cut
452              
453             sub unregister_device {
454 0     0 1   my $self = shift;
455 0           my %args = validate(@_, { hwid => { type => SCALAR }, });
456 0           my %options = ($self->_app, %args);
457 0           $self->_post_data(data => \%options, api_method => 'unregisterDevice');
458             }
459              
460             =head2 set_tags
461              
462             $pw->set_tags(
463             hwid => 'device id',
464             tags => {
465             tag1 => 'konstantinos_atreides',
466             tag2 => 42,
467             tag3 => 'spice_mining',
468             tag4 => 3.14
469             }
470             );
471              
472             Sets tags for the device
473              
474             Parameters:
475              
476             =over
477              
478             =item hwid
479              
480             Hardware device id used in L function call
481              
482             =item tags
483              
484             Tags to set against the device
485              
486             =back
487              
488             =cut
489              
490             sub set_tags {
491 0     0 1   my $self = shift;
492 0           my %args = validate(
493             @_,
494             { hwid => { type => SCALAR },
495             tags => { type => HASHREF },
496             }
497             );
498 0           my %options = ($self->_app, %args);
499 0           $self->_post_data(data => \%options, api_method => 'setTags');
500             }
501              
502             =head2 set_badge
503              
504             $pw->set_badge(
505             hwid => 'device id',
506             badge => 5
507             );
508              
509             B: Only works on iOS devices
510              
511             Set current badge value for the device to let auto-incrementing badges work
512             properly.
513              
514             Parameters:
515              
516             =over
517              
518             =item hwid
519              
520             Hardware device id used in L function call
521              
522             =item badge
523              
524             Current badge on the application to use with auto-incrementing badges
525              
526             =back
527              
528             =cut
529              
530             sub set_badge {
531 0     0 1   my $self = shift;
532 0           my %args = validate(
533             @_,
534             { hwid => { type => SCALAR },
535             badge => { type => SCALAR },
536             }
537             );
538 0           my %options = ($self->_app, %args);
539 0           $self->_post_data(data => \%options, api_method => 'setBadge');
540             }
541              
542             =head2 push_stat
543              
544             $pw->push_stat(
545             hwid => 'device id',
546             hash => 'hash'
547             );
548              
549             Register push open event.
550              
551             Parameters:
552              
553             =over
554              
555             =item hwid
556              
557             Hardware device id used in L function call
558              
559             =item hash
560              
561             Hash tag received in push notification
562              
563             =back
564              
565             =cut
566              
567             sub push_stat {
568 0     0 1   my $self = shift;
569 0           my %args = validate(
570             @_,
571             { hwid => { type => SCALAR },
572             hash => { type => SCALAR },
573             }
574             );
575 0           my %options = ($self->_app, %args);
576 0           $self->_post_data(data => \%options, api_method => 'pushStat');
577             }
578              
579             =head2 get_nearest_zone
580              
581             $pw->get_nearest_zone(
582             hwid => 'device id',
583             lat => 10.12345,
584             lng => 28.12345,
585             );
586              
587             Records device location for geo push notifications
588              
589             Parameters:
590              
591             =over
592              
593             =item hwid
594              
595             Hardware device id used in L function call
596              
597             =item lat
598              
599             Latitude of the device
600              
601             =item lng
602              
603             Longitude of the device
604              
605             =back
606              
607             =cut
608              
609             sub get_nearest_zone {
610 0     0 1   my $self = shift;
611 0           my %args = validate(
612             @_,
613             { hwid => { type => SCALAR },
614             lat => { type => SCALAR },
615             lng => { type => SCALAR }
616             }
617             );
618 0           my %options = ($self->_app, %args);
619 0           $self->_post_data(data => \%options, api_method => 'getNearestZone');
620             }
621              
622             sub _url {
623 0     0     my $self = shift;
624 0           my ($method) = validate_pos(@_, 1);
625 0           return "$self->{api_url}/$method";
626             }
627              
628             sub _post_data {
629 0     0     my $self = shift;
630 0           my %args = validate(
631             @_,
632             { data => 1,
633             api_method => 1
634             }
635             );
636 0           my $data = shift;
637 0           my $post_body = to_json({ request => $args{data} });
638 0           my $furl = $self->{furl};
639 0           my $res = $furl->post($self->_url($args{api_method}),
640             ['Content-Type' => 'application/json'], $post_body);
641 0           my $json = $res->content;
642 0     0     my $status = try { from_json($json) }
643 0     0     catch { croak "Couldn't parse response from api as json: " . $_ };
  0            
644              
645 0           my $code = $res->code;
646 0 0         my $api_code = $status ? $status->{status_code} : undef;
647              
648 0 0 0       if ($code == 200 && $api_code == 200) {
649 0           return $status;
650             }
651             else {
652             # error
653 0 0         return $status if ($self->{error_mode} eq 'manual');
654 0   0       my $error_msg = $Errors{ $args{api_method} }{$code}{ $api_code || 'n/a' };
655 0 0         croak "Got error "
    0          
656             . ($api_code ? "$api_code " : '')
657             . qq[from Pushwoosh API method '$args{api_method}']
658             . ($error_msg ? ": $error_msg" : '');
659             }
660             }
661              
662             =head1 TESTING
663              
664             Since the Pushwoosh API is only available for users of its premium service, the
665             tests will not run without a valid application code and API token. If you want
666             to run the tests, you must set two environment variables with your credentials,
667             eg:
668              
669             PUSHWOOSH_APP_CODE=12345-12345 PUSHWOOSH_API_TOKEN=your_api_key perl t/01-simple.t
670              
671             =head1 AUTHOR
672              
673             Mike Cartmell, C<< >>
674              
675             =head1 LICENSE AND COPYRIGHT
676              
677             Copyright 2013 Mike Cartmell.
678              
679             This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
680              
681             See http://dev.perl.org/licenses/ for more information.
682              
683             =cut
684              
685             1;