File Coverage

blib/lib/SMS/Send/DistributeSMS.pm
Criterion Covered Total %
statement 74 182 40.6
branch 14 54 25.9
condition 2 11 18.1
subroutine 10 18 55.5
pod 2 4 50.0
total 102 269 37.9


)##) { )##) {
line stmt bran cond sub pod time code
1             package SMS::Send::DistributeSMS;
2              
3 2     2   26492 use warnings;
  2         6  
  2         99  
4 2     2   13 use strict;
  2         4  
  2         192  
5              
6             =head1 NAME
7              
8             SMS::Send::DistributeSMS - SMS::Send DistributeSMS Driver
9              
10             =head1 VERSION
11              
12             Version 0.01
13              
14             =cut
15              
16             our $VERSION = '0.01';
17              
18             =head1 SYNOPSIS
19            
20             # Create a sender
21             my $send = SMS::Send->new('DistributeSMS'
22             _account_no => '1234',
23             _login => 'login',
24             _password => 'password',
25             _proxy => 'http://host:port/', # optional.
26             _verbose => 1, # optional. for debugging purposes only.
27             );
28            
29             # Send a message
30             $send->send_sms(
31             text => 'Hi there',
32             to => '+61-400-111-222',
33             _from => 'TEST',
34             );
35              
36             # Get send status (NOTE: specific to this driver)
37             my @status = $sms->status_sms();
38             print "status: $status[0] (state: $status[1])\n";
39              
40             =head1 DESCRIPTION
41              
42             SMS::Send::DistributeSMS is a driver for L for
43             the SMS gateway at www.distributesms.com.au.
44              
45             It currently supports the ability to send an SMS to any country
46             in the world. This is possible by including a translation map
47             from international dialing codes to DistributeSMS specific IDs,
48             requiring no intervention or assistance on the programmer's behalf.
49              
50             As an added bonus, retrieving the send status is possible,
51             although specific to this driver only.
52              
53             Bugs, fixes, flames, rants, patches and messages are welcome.
54              
55             =head1 AUTHOR
56              
57             David Sobon, C<< >>
58              
59             =head1 BUGS
60              
61             Please report any bugs or feature requests to
62             C, or through the web
63             interface at
64             L.
65             I will be notified, and then you'll automatically be notified of
66             progress on your bug as I make changes.
67              
68             =head1 SUPPORT
69              
70             You can find documentation for this module with the perldoc command.
71              
72             perldoc SMS::Send::DistributeSMS
73              
74              
75             You can also look for information at:
76              
77             =over 4
78              
79             =item * RT: CPAN's request tracker
80              
81             L
82              
83             =item * AnnoCPAN: Annotated CPAN documentation
84              
85             L
86              
87             =item * CPAN Ratings
88              
89             L
90              
91             =item * Search CPAN
92              
93             L
94              
95             =back
96              
97              
98             =head1 ACKNOWLEDGEMENTS
99              
100              
101             =head1 COPYRIGHT & LICENSE
102              
103             Copyright 2008 David Sobon, all rights reserved.
104              
105             This program is free software; you can redistribute it and/or modify it
106             under the same terms as Perl itself.
107              
108              
109             =cut
110              
111 2     2   2074 use URI::Escape;
  2         3356  
  2         162  
112 2     2   14 use base 'SMS::Send::Driver';
  2         4  
  2         3770  
113              
114             require LWP::UserAgent;
115             require HTTP::Cookies;
116              
117             #####################################################################
118             # Constructor
119              
120             sub new {
121 1     1 1 86 my $class = shift;
122 1         7 my %args = @_;
123              
124             #
125             # Create the object
126             #
127 1         18 my $self = bless {
128             ua => undef,
129             cj => undef,
130             verbose => $args{'_verbose'},
131             proxy => $args{'_proxy'},
132             messages => [],
133             private => \%args,
134              
135             account_no => $args{'_account_no'},
136             login => $args{'_login'},
137             password => $args{'_password'},
138              
139             # private variables.
140             _MessageID => 0,
141             _loggedin => 0,
142             }, $class;
143              
144 1         4 return $self;
145             }
146              
147             #####################################################################
148             # PUBLIC METHODS
149              
150             #####
151             # Usage: $class->send_sms(%args);
152             # Return: 1 on success, croak-before-return on failure.
153             #
154             sub send_sms {
155 0     0 1 0 my $self = shift;
156 0         0 my %hash = @_;
157              
158             # error: to
159 0 0       0 if (!$hash{'to'}) {
160 0         0 Carp::croak("[send_sms] to number not specified");
161             }
162              
163             # login.
164 0         0 $self->_login();
165 0         0 $self->_getCredits();
166              
167             # post.
168 0         0 my $url = "http://www.distributesms.com.au/cgi-bin/sm.pl";
169 0         0 my %dial = $self->_getDialInfoByNumber( $hash{'to'} );
170 0         0 $hash{'to'} =~ s#^\+$dial{'DialCode'}##;
171              
172 0   0     0 my %post = (
173             PhoneNumber => $hash{'to'},
174             ReplyTo => $hash{'_from'} || '',
175             Message => uri_escape( $hash{'text'} ),
176             DialCodeSeq => $dial{'DialCodeSeq'},
177             PhoneGroup => "",
178             Send => 'send',
179             );
180              
181             # error: dialcodeseq could not be determined.
182 0 0       0 if (!$post{'DialCodeSeq'}) {
183 0         0 Carp::croak("send_sms: could not determine DialCodeSeq from $hash{'to'}");
184             }
185              
186             # send.
187 0         0 $self->print('verbose', "[send_sms] sending message:");
188 0         0 $self->print('verbose', "[send_sms] to: $post{'PhoneNumber'}");
189 0 0       0 $self->print('verbose', "[send_sms] from: $post{'ReplyTo'}") if ($post{'ReplyTo'});
190 0         0 $self->print('verbose', "[send_sms] dialcode: $post{'DialCodeSeq'}");
191 0         0 $self->print('verbose', "[send_sms] message: $post{'Message'}");
192 0         0 my $res = $self->_getURL($url, \%post);
193              
194             # error: check if send failed.
195 0 0       0 if ($res->content !~ /Processed/) {
196 0         0 my $data = $res->content();
197 0         0 $data =~ s#<\/?(body|html)>##gi;
198              
199 0         0 Carp::croak("send_sms: could not send sms - $data");
200             }
201              
202             # if successful, get the current message ID, so we can
203             # determine the message status.
204 0         0 $self->_setLastMessageID();
205              
206 0         0 return 1;
207             }
208              
209             #####
210             # Usage: $class->status_sms([$mid]);
211             # Args: $mid - if not set, $class->{_MessageID} is used.
212             # Return: failure: array: current status string, state (YES/NO).
213             # Return: success: array: (undef, undef)
214             # Note: this method is specific to DistributeSMS as there
215             # is no standardized way to obtain whether an sms
216             # was successfully delivered or not.
217             # to be called $MODULE->_OBJECT->status_sms($mid);
218             #
219             sub status_sms {
220 0     0 0 0 my $class = shift;
221 0   0     0 my ($mid) = @_ || $class->{'_MessageID'};
222              
223             # error: no mid specified.
224 0 0       0 if (!$mid) {
225 0         0 Carp::croak("error: status_sms: no messageid specified");
226             }
227              
228             # if not logged in yet, log in!
229 0 0       0 if (!$class->{'_loggedin'}) {
230 0         0 $class->_login();
231             }
232              
233             # todo: check if $mid is valid?
234 0         0 my $hashref = $class->_getMessageIDStatus($mid);
235 0         0 my %status = %{ $hashref };
  0         0  
236              
237             # valid status strings.
238 0         0 my @_status = (
239             'Scheduled',
240             'Sent to Gateway',
241             'Delivered to Network',
242             'Delivered to Phone'
243             );
244              
245             # return last status.
246 0         0 for (my $i=0; $i
247 0         0 my $status = $_status[$#_status - $i];
248 0         0 my $state = $status{ $status };
249 0 0       0 next unless ($state);
250              
251 0         0 return ($status, $state);
252             }
253              
254 0         0 return (undef, undef);
255             }
256              
257             #####
258             # Usage: $class->print($level, $string);
259             # Args:
260             # $level - 'verbose'
261             # $string - string to print.
262             # Return: undef or return code from printf()
263             #
264             sub print {
265 14     14 0 28 my $class = shift;
266 14         21 my ($level, $string) = @_;
267              
268             # error: level is NULL.
269 14 50       28 if (!$level) {
270 0         0 Carp::croak("DistributeSMS::print: no level specified.");
271             }
272              
273             # error: invalid level.
274 14 50       56 if ($level !~ /^(verbose)$/) {
275 0         0 Carp::croak("DistributeSMS::print: invalid level: $level");
276             }
277              
278             # error: string is NULL.
279 14 50       28 if (!$string) {
280 0         0 Carp::croak("DistributeSMS::print: string is NULL.");
281             }
282              
283             # do not print unless this level has been set via the new constructor
284 14 50       50 return unless ($class->{ $level });
285              
286 0         0 printf "::: %s\n", $string;
287             }
288              
289             #####################################################################
290             # PRIVATE METHODS
291             #####################################################################
292              
293             #####
294             # Usage: $class->_getMessageIDStatus( $MessageID );
295             # Return: %hash reference of status and states.
296             #
297             sub _getMessageIDStatus {
298 0     0   0 my $class = shift;
299 0         0 my ($mid) = @_;
300 0         0 my $url = "http://www.distributesms.com.au/cgi-bin/md.pl?mls=$mid";
301 0         0 my $res = $class->_getURL($url);
302 0         0 my $data = $res->content();
303 0         0 my %status;
304              
305 0         0 $class->print('verbose', "[getMessageIDStatus] length(data) == ".length($data) );
306              
307             # we could use HTML::TreeBuilder... but that is just overkill!
308 0         0 while ($data =~ s#(
309 0         0 my $row = $1;
310 0         0 my @col;
311             ### $class->print('verbose', "[getMessageIDStatus] line: $row");
312              
313 0         0 while ($row =~ s#(.*?)(
314 0         0 $class->print('verbose', "[getMessageIDStatus] col: $1");
315 0         0 push(@col, $1);
316             }
317              
318 0         0 my $status = $col[0];
319 0         0 my $state = $col[2];
320              
321             # status:
322             # Scheduled
323             # Sent to Gateway
324             # Delivered to Network
325             # Delivered to Phone
326              
327 0         0 $class->print('verbose', "[getMessageIDStatus] status{ $status } = $state;");
328              
329 0         0 $status{ $status } = $state;
330             }
331              
332 0         0 return \%status;
333             }
334              
335             #####
336             # Desc: get the last/current message ID and store it privately.
337             # Usage: $class->_getLastMessageID();
338             # Return: $MessageID
339             #
340             sub _getLastMessageID {
341 0     0   0 my $class = shift;
342 0         0 return $class->{'_MessageID'};
343             }
344              
345             #####
346             # Desc: set the last/current message ID and store it privately.
347             # Usage: $class->_setLastMessageID();
348             # Return: 0 on failure, 1 on success.
349             # Note: MessageID is determined within the function itself, not provided
350             # as an input. We are misleading!
351             #
352             sub _setLastMessageID {
353 0     0   0 my $class = shift;
354 0         0 my $url = "http://www.distributesms.com.au/cgi-bin/ml.pl";
355 0         0 my $res = $class->_getURL($url);
356              
357 0 0       0 if ($res->content() !~ /DeliveryStatus\('\/cgi-bin\/md.pl\?mls=(\d+)'/) {
358             # todo: warning message?
359 0         0 return 0;
360             }
361              
362 0         0 $class->{'_MessageID'} = $1;
363              
364 0         0 return 1;
365             }
366              
367             #####
368             # Usage: $class->_getDialInfoByNumber( $number );
369             # Return: undef on failure.
370             # Return: %hash on success:
371             # 'CountryCode' - ISO3166-1-alpha-2 country code.
372             # 'CountryName' - country string.
373             # 'DialCodeSeq' - DistributeSMS specific integer.
374             # 'DialCode' - international dial code.
375             #
376             sub _getDialInfoByNumber {
377 0     0   0 my $class = shift;
378 0         0 my ($number) = @_;
379 0         0 $number =~ s/^\+//;
380              
381 0         0 while () {
382 0         0 chop;
383              
384 0 0       0 next unless (/^(\S{2}):(.*?):(\d+):(\d+)/);
385 0         0 my %hash = (
386             'CountryCode' => $1,
387             'CountryName' => $2,
388             'DialCodeSeq' => $3,
389             'DialCode' => $4,
390             );
391              
392 0 0       0 if ($number =~ /^$hash{'DialCode'}/) {
393 0         0 $class->print('verbose', "[getDialInfoByNumber] number $number matched dialcode $hash{'DialCode'}");
394 0         0 return %hash;
395             }
396             }
397              
398             # error.
399 0         0 Carp::croak("getDialInfoByNumber: could not determine dial info by $number");
400             }
401              
402             #####
403             # Usage: $class->_login();
404             # Return: 1 for failure, 0 for success.
405             #
406             sub _login {
407 1     1   3014 my $class = shift;
408              
409 1 50       7 if ($class->{'_loggedin'}) {
410 0         0 $class->print('verbose', "[login] already logged in!");
411 0         0 return 0;
412             }
413              
414 1         6 $class->print('verbose', "[login] logging in");
415              
416             #
417             # error handling.
418             #
419              
420             # error: no account_no
421 1 50       4 if (!$class->{'account_no'}) {
422 0         0 Carp::croak("login: no account_no specified");
423             }
424              
425             # error: user
426 1 50       4 if (!$class->{'login'}) {
427 0         0 Carp::croak("login: no login specified");
428             }
429              
430             # error: password.
431 1 50       6 if (!$class->{'password'}) {
432 0         0 Carp::croak("login: no password specified");
433             }
434              
435             #
436             # post details.
437             #
438 1         11 my %post = (
439             AccountNo => $class->{'account_no'},
440             UserId1 => $class->{'login'},
441             Password1 => $class->{'password'},
442             Remember1 => 1,
443             OK1 => 'ok',
444             Retry => '',
445             );
446              
447             #
448             # login.
449             #
450 1         4 $class->print('verbose', "[login] logging in with");
451 1         5 $class->print('verbose', "[login] account number: ".$post{'AccountNo'} );
452 1         8 $class->print('verbose', "[login] username: ".$post{'UserId1'} );
453 1         5 $class->print('verbose', "[login] password: ".$post{'Password1'} );
454              
455 1         3 my $url = "http://www.distributesms.com.au/cgi-bin/li.pl";
456 1         6 my $res = $class->_getURL($url, \%post);
457              
458             #
459             # error: login failed.
460             #
461 1 50       6 if ($res->content() !~ /sm\.pl/) {
462 1         16 my $data = $res->content();
463             # $data =~ s#<\/?(body|html)>##gi;
464              
465 1         15 $class->print('verbose', "[login] ".$data);
466              
467 1         34 die("login: could not log in");
468             }
469              
470 0         0 $class->print('verbose', "[login] successful!");
471 0         0 $class->{'_loggedin'} = 1;
472              
473 0         0 return 1;
474             }
475              
476             #####
477             # Usage: $class->_getCredits();
478             # Return: 0 for failure, # of credits left for success.
479             #
480             sub _getCredits {
481 0     0   0 my $class = shift;
482 0         0 my $url = "http://www.distributesms.com.au/cgi-bin/sm.pl";
483 0         0 my $res = $class->_getURL($url);
484 0         0 my $check = 0;
485              
486 0 0       0 $check++ if ($res->content() =~ /Pre-Paid Messages Left/);
487 0 0       0 $check++ if ($res->content() =~ /Leave blank to send immediately/);
488              
489             #
490             # error: login or something weird failed.
491             #
492 0 0       0 if (!$check) {
493 0         0 my $data = $res->content();
494 0         0 $data =~ s#<\/?(body|html)>##gi;
495              
496 0         0 Carp::croak("getCredits: login failed - $data");
497             }
498              
499             # todo: change so it returns # of credits left.
500 0         0 return 1;
501             }
502              
503             #####
504             # Usage: $class->_getURL($url, $postref);
505             # Return: $res object
506             #
507             sub _getURL {
508 1     1   3 my $class = shift;
509 1         3 my ($url, $postref) = @_;
510 1         2 my $req;
511             my $res;
512              
513             # initialize.
514 1 50 33     8 if (!$class->{'ua'} || !$class->{'cj'}) {
515 1         5 $class->{'cj'} = $class->_newCJ();
516 1         4 $class->{'ua'} = $class->_newUA();
517             }
518              
519             # todo: check if url exists and is valid.
520             # todo: if postref exists and is a hash reference.
521              
522 1         9 $class->print('verbose', "[getURL] url: $url");
523              
524 1 50       5 if ($postref) {
525 1         29 my %post = %{ $postref };
  1         10  
526 1         4 my $post = join('&', map { $_.'='.$post{$_} } keys %post);
  6         18  
527              
528 1         11 foreach (sort keys %post) {
529 6         21 $class->print('verbose', "[getURL] post: $_ => $post{$_}");
530             }
531 1         6 $class->print('verbose', "[getURL] POST: $post");
532              
533 1         9 $req = HTTP::Request->new('POST', $url);
534 1         10313 $req->content_type('application/x-www-form-urlencoded');
535 1         59 $req->content($post);
536             } else {
537 0         0 $req = HTTP::Request->new('GET', $url);
538             }
539              
540             # todo: convert all $class->{'cj'} to $cj = $class->_getCJ();
541 1         36 $class->{'cj'}->add_cookie_header($req);
542 1         708 $res = $class->{'ua'}->request($req);
543 1         4020587 $class->{'cj'}->extract_cookies($res);
544              
545             $class->{'cj'}->scan( sub {
546 0     0   0 $class->{'cj'}->set_cookie(@_);
547 1         67 } );
548              
549             # error: http 500 or something catastrophic.
550 1 50 33     16 if ($res->code() =~ /^5/ && $res->content() =~ /timeo/i) {
551 0         0 Carp::croak("getURL: proxy is dead; please update.");
552             }
553              
554 1         38 return $res;
555             }
556              
557             #####
558             # Usage: $class->_newCJ();
559             # Return: $cj object.
560             #
561             sub _newCJ {
562 1     1   3 my $class = shift;
563 1         9 my $cj = new HTTP::Cookies;
564              
565 1         25 return $cj;
566             }
567              
568             #####
569             # Usage: $class->_newUA();
570             # Return: $ua object.
571             #
572             sub _newUA {
573 1     1   2 my $class = shift;
574 1         11 my $ua = new LWP::UserAgent;
575              
576 1         5330 $ua->agent("Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)");
577 1         81 $ua->cookie_jar($class->{'cj'});
578 1         111 $ua->timeout(10);
579              
580             # set useragent.
581 1 50       23 if ($class->{'agent'}) {
582 0         0 $class->print('verbose', "[newUA] setting useragent to ".$class->{'agent'} );
583 0         0 $ua->agent( $class->{'agent'} );
584             }
585              
586             # set proxy.
587 1 50       4 if ($class->{'proxy'}) {
588 0         0 $class->print('verbose', "[newUA] setting proxy to ".$class->{'proxy'} );
589 0         0 $ua->proxy('http', $class->{'proxy'});
590             }
591              
592 1         5 return $ua;
593             }
594              
595             1;
596              
597             ###########################################################
598             # DATA:
599             ###########################################################
600              
601             __DATA__