File Coverage

blib/lib/WebService/Sprint.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package WebService::Sprint;
2              
3 1     1   27182 use warnings;
  1         3  
  1         35  
4 1     1   8 use strict;
  1         2  
  1         58  
5              
6             =head1 NAME
7              
8             WebService::Sprint - an interface to Sprint's web services
9              
10             =head1 VERSION
11              
12             Version 0.50
13              
14             =cut
15              
16             our $VERSION = '0.50';
17              
18 1     1   519 use DateTime;
  0            
  0            
19             use Digest::MD5 qw(md5_hex);
20             use LWP::Simple;
21             use LWP::UserAgent;
22             use JSON;
23             use URI;
24              
25             my %DEFAULTS = (
26             base_url =>
27             "http://sprintdevelopersandbox.com/developerSandbox/resources/v1/",
28             user_agent => "WebService::Sprint",
29             );
30              
31             my %SERVICES = (
32             location => 'location.json',
33             presence => 'presence.json',
34             perimeter => 'geofence/checkPerimeter.json',
35             devices => 'devices.json',
36             device => 'device.json',
37             );
38              
39             =head1 DESCRIPTION
40              
41             Provides an object-oriented interface to Sprint's developer web services, including geolocation of devices on Sprint's CDMA network in the United States.
42              
43             Disclaimer: I am not affiliated with Sprint. This is an implementation of their publicly-available specifications. Sprint is probably a registered trademark, and is used here to highlight that this implementation is specific to the Sprint network.
44              
45             Implements some features of Sprint's developer web services. For more information, see:
46              
47             =over 4
48              
49             =item * L
50              
51             =item * L
52              
53             =item * L
54              
55             =back
56              
57             Currently supports:
58              
59             =over 4
60              
61             =item * Presence
62              
63             =item * Location (3G)
64              
65             =item * Geofence perimeter check
66              
67             =item * User management -- retrieving list of devices, adding or removing devices
68              
69             =back
70              
71             Does not support:
72              
73             =over 4
74              
75             =item * All other geofence operations
76              
77             =item * SMS
78              
79             =item * iDen Content Uploader
80              
81             =back
82              
83             =head1 SYNOPSIS
84              
85             use WebService::Sprint;
86              
87             my $ws = WebService::Sprint->new(
88             key => '0123456789abcdef',
89             secret => 'fedcba98765432100123456789abcdef',
90             );
91              
92             # Get the list of devices associated with your developer account
93             my $devices = $ws->get_devices(
94              
95             # Optionally limit to devices in a particular state
96             type => 'approved',
97             );
98              
99             # Request addition of a device to your account (requires user approval
100             # via SMS)
101             my $device = $ws->add_device(
102             mdn => '2225551212',
103             );
104              
105             # Remove a device from your account
106             my $device = $ws->del_device(
107             mdn => '2225551212',
108             );
109              
110             # Check whether a given device is present on the network
111             my $presence = $ws->get_presence(
112             mdn => '2225551212',
113             );
114              
115             # Fetch the network location of a single device
116             my $location = $ws->get_location(
117             mdn => '2225551212',
118             );
119              
120             # Fetch the location of multiple devices (issues multiple queries
121             # behind the scenes, serially)
122             my @locations = $ws->get_location(
123             mdn => [qw(2225551212 8885551234)],
124             );
125              
126             # Check whether a device is within a given geofence
127             my $location = $ws->check_perimeter(
128             mdn => '2225551212',
129              
130             # Geofence coordinates in Decimal Degrees
131             latitude => 45.000,
132             longitude => -120.000,
133              
134             # Radius in meters (server requires at least 2000 meters)
135             radius => 5000,
136             );
137              
138             =head1 METHODS
139              
140             =head2 new
141              
142             Instantiates a Web Service object. Named arguments include:
143             B: Your Sprint-assigned developer key
144             B: Your Sprint-assigned shared secret
145             B: The base URL for Sprint services (defaults to L)
146             B: HTTP user agent used (defaults to L)
147              
148             =cut
149              
150             sub new {
151             die "Arguments must be valid name-value pairs"
152             unless @_ % 2;
153             my ( $class, %args ) = @_;
154              
155             my $self = {};
156              
157             _get_defaults( $self, \%args );
158              
159             bless $self, $class;
160              
161             return $self;
162              
163             }
164              
165             sub _get_defaults {
166             my ( $dst, $src ) = @_;
167              
168             if ( !defined $dst
169             || ref $dst ne 'HASH'
170             || !defined $src
171             || ref $src ne 'HASH' )
172             {
173             die "Invalid parameters";
174             }
175              
176             while ( my ( $name, $value ) = each %DEFAULTS ) {
177             $dst->{$name} = $value;
178             }
179              
180             while ( my ( $name, $value ) = each %{$src} ) {
181             $dst->{$name} = $value;
182             }
183              
184             return;
185             }
186              
187             =head2 get_devices
188              
189             Given no arguments, returns a hashref of devices associated with your developer account, indicating status.
190             Given the argument B with a valid 10-digit phone number, returns information about that device only.
191             Given the argument B (I, I, I, I, I), returns information about only that subset of devices associated with your account.
192              
193             =cut
194              
195             sub get_devices {
196             my ( $self, %args ) = @_;
197              
198             my %params;
199              
200             my $mdn;
201             if ( defined $args{mdn} ) {
202             if ( $mdn = _clean_mdn( $args{mdn} ) ) {
203             $params{type} = 'mdn';
204             $params{mdn} = $mdn;
205             }
206             else {
207             die "Invalid MDN: $args{mdn}\n";
208             }
209             }
210             else {
211             if ( defined $args{type} ) {
212             $params{type} =
213             ( $args{type} =~ m/^p(?:ending)?/i ) ? 'p'
214             : ( $args{type} =~ m/^dec(?:lined)?/i ) ? 'x'
215             : ( $args{type} =~ m/^del(?:eted)?/i ) ? 'd'
216             : ( $args{type} =~ m/^ap(?:proved)/i ) ? 'a'
217             : ( $args{type} =~ m/^al(?:l)/i ) ? 'null'
218             : die "Invalid type: $args{type}\n";
219             }
220             }
221              
222             my $devices = $self->issue_query(
223             service => 'devices',
224             params => \%params,
225             );
226              
227             my %devices = (
228             original => $devices,
229             timestamp => time,
230             );
231             my @devices;
232              
233             $devices{auth_status} = lc _best_match( $devices, qr/^auth.?status/i );
234             my $device_list = $devices->{devices};
235             if ( defined $device_list ) {
236             if ( ref $device_list eq 'HASH' ) {
237             while ( my ( $status, $list ) = each %{$device_list} ) {
238             if ( defined $list && ref $list eq 'ARRAY' ) {
239             foreach my $mdn ( @{$list} ) {
240             push(
241             @devices,
242             {
243             mdn => $mdn,
244             status => lc $status,
245             },
246             );
247             }
248             }
249             }
250             }
251             elsif ( ref $device_list eq 'ARRAY' ) {
252             foreach my $mdn ( @{$device_list} ) {
253             push(
254             @devices,
255             {
256             mdn => $mdn,
257             status => $devices{auth_status},
258             },
259             );
260             }
261             }
262             else {
263             $devices{devices} = $device_list;
264             }
265             }
266              
267             $devices{devices} = \@devices;
268             $devices{username} = _best_match( $devices, qr/^username/i );
269             $devices{error} = _best_match( $devices, qr/^error/i );
270              
271             return \%devices;
272             }
273              
274             =head2 add_device
275              
276             Given the argument B with a valid 10-digit phone number, attempts to add the user to your account. Returns a hashref including status of the request.
277              
278             =cut
279              
280             sub add_device {
281             my ( $self, %args ) = @_;
282              
283             my %params = ( method => 'add', );
284              
285             my $mdn;
286             if ( defined $args{mdn} ) {
287             if ( $mdn = _clean_mdn( $args{mdn} ) ) {
288             $params{mdn} = $mdn;
289             }
290             else {
291             die "Invalid MDN: $args{mdn}\n";
292             }
293             }
294             else {
295             return;
296             }
297              
298             my $device = $self->issue_query(
299             service => 'device',
300             params => \%params,
301             );
302              
303             my %device = (
304             original => $device,
305             timestamp => time,
306             );
307              
308             $device{mdn} = lc _best_match( $device, qr/^mdn$/i ) || $params{mdn};
309             $device{status} = lc _best_match( $device, qr/^message/i );
310             $device{error} = _best_match( $device, qr/^error/i );
311              
312             return \%device;
313             }
314              
315             =head2 del_device
316              
317             Given the argument B with a valid 10-digit phone number, attempts to remove the user from your account. Returns a hashref including status of the request.
318              
319             =cut
320              
321             sub del_device {
322             my ( $self, %args ) = @_;
323              
324             my %params = ( method => 'delete', );
325              
326             my $mdn;
327             if ( defined $args{mdn} ) {
328             if ( $mdn = _clean_mdn( $args{mdn} ) ) {
329             $params{mdn} = $mdn;
330             }
331             else {
332             die "Invalid MDN: $args{mdn}\n";
333             }
334             }
335             else {
336             return;
337             }
338              
339             my $device = $self->issue_query(
340             service => 'device',
341             params => \%params,
342             );
343              
344             my %device = (
345             original => $device,
346             timestamp => time,
347             );
348              
349             $device{mdn} = lc _best_match( $device, qr/^mdn$/i ) || $params{mdn};
350             $device{status} = lc _best_match( $device, qr/^message/i );
351             $device{error} = _best_match( $device, qr/^error/i );
352              
353             return \%device;
354             }
355              
356             =head2 get_presence
357              
358             Given the argument B as a single or list of 10-digit phone numbers, returns a hashref (or list of hashrefs) with detailed information about the presence of the requested device on the Sprint network.
359              
360             This call should not use your credits.
361              
362             =cut
363              
364             sub get_presence {
365             my ( $self, %args ) = @_;
366              
367             if ( !defined $args{mdn} ) {
368             return;
369             }
370              
371             if ( ref $args{mdn} eq 'ARRAY' ) {
372             my @response;
373             foreach my $mdn ( @{ $args{mdn} } ) {
374             push( @response, $self->get_presence( mdn => $mdn ) );
375             }
376             return @response;
377             }
378              
379             if ( my $mdn = _clean_mdn( $args{mdn} ) ) {
380             my $presence = $self->issue_query(
381             service => 'presence',
382             params => { mdn => $mdn, },
383             );
384              
385             my %presence = (
386             original => $presence,
387             mdn => $mdn,
388             timestamp => time,
389             );
390              
391             $presence{error} = _best_match( $presence, qr/^error/i );
392              
393             my $reachable = _best_match( $presence, qr/^status/i );
394             $presence{reachable} =
395             ( $reachable && $reachable =~ m/^reachable/i ) ? 1 : 0;
396              
397             my $response_mdn = _best_match( $presence, qr/^mdn/i );
398             if ( defined $response_mdn && $mdn ne $response_mdn ) {
399             die "Response received for incorrect MDN: $response_mdn\n";
400             }
401              
402             return \%presence;
403             }
404             else {
405             die "Invalid MDN: $args{mdn}\n";
406             }
407             }
408              
409             =head2 get_location
410              
411             Given the argument B as a single or list of 10-digit phone numbers, returns a hashref (or list of hashrefs) with detailed information about the location of the requested device. This usually returns a network-determined low-precision location for the device, and completes within about 5 seconds. It usually does I activate the device's GPS receiver.
412              
413             B
414              
415             =cut
416              
417             sub get_location {
418             my ( $self, %args ) = @_;
419              
420             if ( !defined $args{mdn} ) {
421             return;
422             }
423              
424             if ( ref $args{mdn} eq 'ARRAY' ) {
425             my @response;
426             foreach my $mdn ( @{ $args{mdn} } ) {
427             push( @response, $self->get_location( mdn => $mdn ) );
428             }
429             return @response;
430             }
431              
432             if ( my $mdn = _clean_mdn( $args{mdn} ) ) {
433             my $location = $self->issue_query(
434             service => 'location',
435             params => { mdn => $mdn, },
436             );
437              
438             my %location = (
439             original => $location,
440             mdn => $mdn,
441             timestamp => time,
442             );
443              
444             $location{error} = _best_match( $location, qr/^error/i );
445             $location{latitude} = _best_match( $location, qr/^lat/i );
446             $location{longitude} = _best_match( $location, qr/^lon/i );
447             $location{accuracy} = _best_match( $location, qr/^accuracy/i );
448             if ( _best_match( $location, qr/^old/i ) ) {
449             $location{old}++;
450             }
451              
452             my $response_mdn = _best_match( $location, qr/^mdn/i );
453             if ( defined $response_mdn && $mdn ne $response_mdn ) {
454             die "Response received for incorrect MDN: $response_mdn\n";
455             }
456              
457             return \%location;
458             }
459             else {
460             die "Invalid MDN: $args{mdn}\n";
461             }
462             }
463              
464             =head2 check_perimeter
465              
466             Works similarly to C, but is intended to determine whether a given device is within a specified geofence. Takes the additional (required) parameters B, B (both in decimal degrees), and B (in meters). Returns location information, the specified geofence, and whether the device is inside the defined fence.
467              
468             This is a distinctly different service call to Sprint, even though it could be implemented with some geo-math around C. Specifically, this service call attempts to obtain a higher-precision location, and consequently, Sprint charges more credits for its use. Usually it will trigger a GPS location request on the device itself, and may take around 40 seconds to complete.
469              
470             B
471              
472             =cut
473              
474             sub check_perimeter {
475             my ( $self, %args ) = @_;
476              
477             if ( !defined $args{mdn} ) {
478             return;
479             }
480              
481             my $latitude = _find_defined( @args{qw(lat latitude)} )
482             or die "Latitude not provided";
483              
484             if ( !_in_range( $latitude, -90, 90 ) ) {
485             die "Invalid latitude: $latitude\n";
486             }
487              
488             my $longitude = _find_defined( @args{qw(lon longitude long)} )
489             or die "Longitude not provided";
490              
491             if ( !_in_range( $longitude, -180, 180 ) ) {
492             die "Invalid longitude: $longitude\n";
493             }
494              
495             my $radius = _find_defined( @args{qw(rad radius range)} )
496             or die "Radius not provided";
497              
498             if ( !_in_range( $radius, 2000, undef ) ) {
499             die "Invalid radius: $radius\n";
500             }
501              
502             if ( ref $args{mdn} eq 'ARRAY' ) {
503             my @mdns = @{ $args{mdn} };
504              
505             my @response;
506              
507             foreach my $mdn (@mdns) {
508             push( @response, $self->check_perimeter( %args, mdn => $mdn, ), );
509             }
510             return @response;
511             }
512              
513             if ( my $mdn = _clean_mdn( $args{mdn} ) ) {
514             my $status = $self->issue_query(
515             service => 'perimeter',
516             params => {
517             mdn => $mdn,
518             lat => $latitude,
519             long => $longitude,
520             rad => $radius,
521             },
522             );
523              
524             my %status = (
525             original => $status,
526             mdn => $mdn,
527             timestamp => time,
528             );
529              
530             $status{error} = _best_match( $status, qr/^error/i );
531             $status{latitude} = _best_match( $status, qr/^lat/i );
532             $status{longitude} = _best_match( $status, qr/^lon/i );
533             $status{accuracy} = _best_match( $status, qr/^accuracy/i );
534             $status{comment} = _best_match( $status, qr/^comment/i );
535              
536             my $inside = _best_match( $status, qr/^currentlocation/i );
537             $status{inside} = ( $inside && $inside =~ m/inside/i ) ? 1 : 0;
538              
539             if ( !$status{error} && $inside =~ qr/fail/i ) {
540             $status{error} = $inside;
541             }
542              
543             $status{perimeter} = {
544             radius => _best_match( $status, qr/^radius/i ) || undef,
545             latitude => _best_match( $status, qr/^glat/i ) || undef,
546             longitude => _best_match( $status, qr/^glong/i ) || undef,
547             };
548              
549             my $response_mdn = _best_match( $status, qr/^mdn/i );
550             if ( defined $response_mdn && $mdn ne $response_mdn ) {
551             die "Response received for incorrect MDN: $response_mdn\n";
552             }
553              
554             return \%status;
555             }
556             else {
557             die "Invalid MDN: $args{mdn}\n";
558             }
559             }
560              
561             =head1 EXTRA METHODS
562              
563             These are underlying methods that may be useful to extend this module for use with additional services.
564              
565             =head2 issue_query
566              
567             Given a list of named arguments, issues a web service request. Calling this method takes care of timestamp and authentication/hashing requirements for you.
568             Provided for your convenience to access herein-unimplemented services.
569              
570             =cut
571              
572             sub issue_query {
573             my ( $self, %args ) = @_;
574              
575             my $url = $self->build_url(%args);
576              
577             #warn "URL: $url\n";
578              
579             my $response = $self->fetch_url( url => $url, );
580              
581             #warn "Response: $response\n";
582              
583             my $output = $self->decode_response( json => $response, );
584              
585             return $output;
586             }
587              
588             =head2 build_url
589              
590             Given a list of named arguments, constructs the service URL, adding the timestamp and hash. Called by issue_query, and provided for your convenience.
591              
592             =cut
593              
594             sub build_url {
595             my ( $self, %args ) = @_;
596              
597             if ( !defined $SERVICES{ $args{service} } ) {
598             die "Invalid service $args{service}\n";
599             }
600              
601             my $dt = DateTime->now( time_zone => 'local', );
602              
603             my %params = (
604             key => $self->get_key,
605             timestamp => $dt->iso8601 . $dt->time_zone_short_name,
606             );
607              
608             while ( my ( $key, $value ) = each %{ $args{params} } ) {
609             $params{$key} = $value;
610             }
611              
612             my $hash = $self->get_hash(%params);
613              
614             my $uri = URI->new( $self->{base_url} . $SERVICES{ $args{service} } );
615              
616             $uri->query_form( %params, sig => $hash, );
617              
618             return $uri;
619             }
620              
621             =head2 fetch_url
622              
623             Given a named argument url, retrieves the URL. If successful, returns the content. If failed, returns the status message. Called by issue_query, and provided for your convenience.
624              
625             =cut
626              
627             sub fetch_url {
628             my ( $self, %args ) = @_;
629              
630             my $url = $args{url}
631             or die "No URL to fetch";
632              
633             my $ua = LWP::UserAgent->new
634             or die "Failed to create a User Agent\n";
635              
636             $ua->agent( $self->{user_agent} );
637              
638             my $req = HTTP::Request->new( GET => $url );
639              
640             my $res = $ua->request($req);
641              
642             if ( $res->is_success ) {
643             return $res->content;
644             }
645             else {
646             die $res->status_line;
647             }
648             }
649              
650             =head2 decode_response
651              
652             Given a named argument json containing the JSON response from a web service query, attempts to decode the JSON into a hash ref. Attempts to remove the extraneous line feeds that appear in some responses.
653              
654             =cut
655              
656             sub decode_response {
657             my ( $self, %args ) = @_;
658              
659             if ( !defined $args{json} ) {
660             die "No response found";
661             }
662              
663             my $output;
664              
665             DECODE_JSON:
666             {
667             eval { $output = decode_json( $args{json} ); };
668             if ($@) {
669             if ( $args{json} =~ tr/\n\r//d ) {
670              
671             # This happens reliably on certain requests, so we just handle it silently now
672             # warn
673             # "Trimmed line feeds from JSON response for compatibility\n";
674             redo DECODE_JSON;
675             }
676             else {
677             die "$@\nRaw JSON: $args{json}\n";
678             }
679             }
680             }
681              
682             return $output;
683             }
684              
685             =head2 get_hash
686              
687             Given a named argument list, orders the keys and calculates the authentication hash, based on the shared secret. This method is called internally when building a request URI, but is provided for your convenience.
688              
689             =cut
690              
691             sub get_hash {
692             my ( $self, %args ) = @_;
693              
694             my $secret = $self->get_secret
695             or die "No secret available";
696              
697             my @hash_data;
698             foreach my $key ( sort keys %args ) {
699             push( @hash_data, $key, $args{$key} );
700             }
701             my $hash = md5_hex( join( '', @hash_data, $secret ) );
702              
703             return $hash;
704             }
705              
706             =head2 get_key
707              
708             Returns the object's stored key. Provided for your convenience.
709              
710             =cut
711              
712             sub get_key {
713             my ($self) = @_;
714              
715             if ( !defined $self->{key} ) {
716             die "Key must be defined!\n";
717             }
718              
719             return $self->{key};
720             }
721              
722             =head2 get_secret
723              
724             Returns the object's stored shared secret. Provided for your convenience.
725              
726             =cut
727              
728             sub get_secret {
729             my ($self) = @_;
730              
731             if ( !defined $self->{secret} ) {
732             die "Secret must be defined!\n";
733             }
734              
735             return $self->{secret};
736             }
737              
738             =head1 AUTHOR
739              
740             Brett T. Warden, C<< >>
741              
742             =head1 BUGS
743              
744             Please report any bugs or feature requests to C, or through
745             the web interface at L. I will be notified, and then you'll
746             automatically be notified of progress on your bug as I make changes.
747              
748             =head1 SUPPORT
749              
750             You can find documentation for this module with the perldoc command.
751              
752             perldoc WebService::Sprint
753              
754              
755             You can also look for information at:
756              
757             =over 4
758              
759             =item * RT: CPAN's request tracker
760              
761             L
762              
763             =item * AnnoCPAN: Annotated CPAN documentation
764              
765             L
766              
767             =item * CPAN Ratings
768              
769             L
770              
771             =item * Search CPAN
772              
773             L
774              
775             =back
776              
777              
778             =head1 ACKNOWLEDGMENTS
779              
780              
781             =head1 LICENSE AND COPYRIGHT
782              
783             Copyright 2011-2012 Brett T. Warden.
784              
785             This program is free software; you can redistribute it and/or modify it
786             under the terms of either: the GNU General Public License as published
787             by the Free Software Foundation; or the Artistic License.
788              
789             See http://dev.perl.org/licenses/ for more information.
790              
791              
792             =cut
793              
794             # Sanitizes an mdn
795             sub _clean_mdn {
796             my ($orig_mdn) = @_;
797              
798             # Strip unseemly characters
799             $orig_mdn =~ s/[\s()]//g;
800              
801             if (
802             my @parts = (
803             $orig_mdn =~
804             m/^(?:\+?1)?[\-\.]?(\d{3})[\-\.]?(\d{3})[\-\.]?(\d{4})$/
805             )
806             )
807             {
808             return join( '', @parts );
809             }
810             else {
811             return;
812             }
813             }
814              
815             # Returns the first defined argument in the argument list
816             sub _find_defined {
817             foreach my $arg (@_) {
818             if ( defined $arg ) {
819             return $arg;
820             }
821             }
822             return;
823             }
824              
825             # Determines whether the supplied number is in the supplied range.
826             sub _in_range {
827             my ( $number, $lower, $upper ) = @_;
828              
829             if ( $number !~ m/^-?\d+(\.\d*)?$/ ) {
830             die "Not a floating point number: $number\n";
831             }
832              
833             if ( defined $lower ) {
834             if ( $number < $lower ) {
835             return;
836             }
837             }
838              
839             if ( defined $upper ) {
840             if ( $number > $upper ) {
841             return;
842             }
843             }
844              
845             return 1;
846             }
847              
848             # Returns the value from the supplied hashref whose key best matches the supplied regex
849             sub _best_match {
850             my ( $h, $re ) = @_;
851              
852             if ( ref $h ne 'HASH' ) {
853             return;
854             }
855              
856             if ( ref $re ne 'Regexp' ) {
857             die "$re is not a Regular Expression";
858             }
859              
860             KEY:
861             foreach my $key ( sort _by_length keys %{$h} ) {
862             if ( $key =~ $re ) {
863             return $h->{$key};
864             }
865             }
866              
867             return;
868             }
869              
870             # Sorting helper function to order arguments by length
871             sub _by_length {
872             my $a_len = 0;
873             my $b_len = 0;
874              
875             if ( defined $a ) {
876             $a_len = length $a;
877             }
878             if ( defined $b ) {
879             $b_len = length $b;
880             }
881              
882             return $a_len <=> $b_len;
883             }
884              
885             1; # End of WebService::Sprint