File Coverage

blib/lib/SMS/AQL.pm
Criterion Covered Total %
statement 53 132 40.1
branch 5 44 11.3
condition 2 17 11.7
subroutine 11 19 57.8
pod 8 8 100.0
total 79 220 35.9


line stmt bran cond sub pod time code
1             package SMS::AQL;
2              
3             # SMS::AQL - Sends text messages via AQL's gateway
4             #
5             # David Precious, davidp@preshweb.co.uk
6             #
7             # $Id$
8              
9              
10 1     1   656 use 5.005000;
  1         3  
  1         29  
11              
12 1     1   3 use strict;
  1         1  
  1         34  
13 1     1   12 use warnings;
  1         1  
  1         29  
14 1     1   2 use Carp;
  1         2  
  1         56  
15 1     1   510 use LWP::UserAgent;
  1         28151  
  1         25  
16 1     1   6 use HTTP::Request;
  1         1  
  1         18  
17 1     1   2 use vars qw($VERSION);
  1         2  
  1         1325  
18              
19             $VERSION = '1.02';
20              
21             my $UNRECOGNISED_RESPONSE = "Unrecognised response from server";
22             my $NO_RESPONSES = "Could not get valid response from any server";
23              
24             =head1 NAME
25              
26             SMS::AQL - Perl extension to send SMS text messages via AQL's SMS service
27              
28             =head1 SYNOPSIS
29              
30             # create an instance of SMS::AQL, passing it your AQL username
31             # and password (if you do not have a username and password,
32             # register at www.aql.com first).
33            
34             $sms = new SMS::AQL({
35             username => 'username',
36             password => 'password'
37             });
38              
39             # other parameters can be passed like so:
40             $sms = new SMS::AQL({
41             username => 'username',
42             password => 'password',
43             options => { sender => '+4471234567' }
44             });
45            
46             # send an SMS:
47            
48             $sms->send_sms($to, $msg) || die;
49            
50             # called in list context, we can see what went wrong:
51             my ($ok, $why) = $sms->send_sms($to, $msg);
52             if (!$ok) {
53             print "Failed, error was: $why\n";
54             }
55            
56             # params for this send operation only can be supplied:
57             $sms->send_sms($to, $msg, { sender => 'bob the builder' });
58            
59             # make a phone call and read out a message:
60             my ($ok, $why) = $sms->voice_push($to, $msg);
61              
62            
63              
64             =head1 DESCRIPTION
65              
66             SMS::AQL provides a nice object-oriented interface to send SMS text
67             messages using the HTTP gateway provided by AQ Ltd (www.aql.com) in
68             the UK.
69              
70             It supports concatenated text messages (over the 160-character limit
71             of normal text messages, achieved by sending multiple messages with
72             a header to indicate that they are part of one message (this is
73             handset-dependent, but supported by all reasonably new mobiles).
74              
75              
76              
77             =head1 METHODS
78              
79             =over
80              
81             =item new (constructor)
82              
83             You must create an instance of SMS::AQL, passing it the username and
84             password of your AQL account:
85              
86             $sms = new SMS::AQL({ username => 'fred', password => 'bloggs' });
87            
88             You can pass extra parameters (such as the default sender number to use,
89             or a proxy server) like so:
90              
91             $sms = new SMS::AQL({
92             username => 'fred',
93             password => 'bloggs',
94             options => {
95             sender => '+44123456789012',
96             proxy => 'http://user:pass@host:port/',
97             },
98             });
99              
100             =cut
101              
102             sub new {
103              
104 1     1 1 353 my ($package, $params) = @_;
105              
106 1 50 33     7 if (!$params->{username} || !$params->{password}) {
107 0         0 warn 'Must supply username and password';
108 0         0 return undef;
109             }
110              
111 1   50     5 my $self = bless { contents => {} } =>
112             ($package || 'SMS::AQL');
113              
114             # get an LWP user agent ready
115 1         3 $self->{ua} = new LWP::UserAgent;
116 1         1639 $self->{ua}->agent("SMS::AQL/$VERSION");
117            
118             # configure user agent to use a proxy, if requested:
119             # TODO: validate supplied proxy details
120 1 50       37 if ($params->{options}->{proxy}) {
121 0         0 $self->{ua}->proxy(['http','https'] => $params->{options}->{proxy});
122             }
123            
124             # remember the username and password
125 1         2 ($self->{user}, $self->{pass}) =
126             ($params->{username}, $params->{password});
127            
128            
129             # remember extra params:
130 1         2 $self->{options} = $params->{options};
131            
132             # the list of servers we can try:
133 1         1 $self->{sms_servers} = [qw(
134             gw.aql.com
135             )];
136            
137 1         2 $self->{voice_servers} = ['vp1.aql.com'];
138            
139             # remember the last server response we saw:
140 1         2 $self->{last_response} = '';
141 1         1 $self->{last_response_text} = '';
142 1         1 $self->{last_error} = '';
143 1         2 $self->{last_status} = 0;
144            
145 1         4 return $self;
146             }
147              
148              
149              
150             =item send_sms($to, $message [, \%params])
151              
152             Sends the message $message to the number $to, optionally
153             using the parameters supplied as a hashref.
154              
155             If called in scalar context, returns 1 if the message was
156             sent, 0 if it wasn't.
157              
158             If called in list context, returns a two-element list, the
159             first element being 1 for success or 0 for fail, and the second
160             being a message indicating why the message send operation
161             failed.
162              
163             You must set a sender, either at new or for each send_sms call.
164              
165             Examples:
166            
167             if ($sms->send_sms('+44123456789012', $message)) {
168             print "Sent message successfully";
169             }
170            
171             my ($ok, $msg) = $sms->send_sms($to, $msg);
172             if (!$ok) {
173             print "Failed to send the message, error: $msg\n";
174             }
175            
176             =cut
177              
178             sub send_sms {
179              
180 0     0 1 0 my ($self, $to, $text, $opts) = @_;
181            
182 0         0 $to =~ s/[^0-9+]//xms;
183              
184             # assemble the data we need to POST to the server:
185 0   0     0 my %postdata = (
186             username => $self->{user},
187             password => $self->{pass},
188             orig => $opts->{sender} || $self->{options}->{sender},
189             to_num => $to,
190             message => $text,
191             );
192            
193 0 0       0 if (!$postdata{orig}) {
194 0         0 $self->{last_error} = "Cannot send message without sender specified";
195 0         0 warn($self->{last_error});
196 0         0 return 0;
197             }
198            
199 0         0 my $response =
200             $self->_do_post($self->{sms_servers},
201             '/sms/postmsg-concat.php', \%postdata);
202            
203 0 0 0     0 if ($response && $response->is_success) {
204 0         0 $self->_check_aql_response_code($response);
205             return wantarray ?
206 0 0       0 ($self->last_status, $self->last_response_text) : $self->last_status;
207             }
208              
209             # OK, we got no response from any of the servers we tried:
210 0         0 $self->_set_no_valid_response;
211 0 0       0 return wantarray ? (0, $self->last_error) : 0;
212            
213             } # end of sub send_sms
214              
215              
216              
217             =item voice_push($to, $message [, \%params])
218              
219             Make a telephone call to the given phone number, using speech synthesis to
220             read out the message supplied.
221              
222             $to and $message are the destination telephone number and the message to read
223             out. The third optional parameter is a hashref of options to modify the
224             behaviour of this method - currently, the only option is:
225              
226             =over 4
227              
228             =item skipintro
229              
230             Skips the introductory message that AQL's system normally reads out. (If you
231             use this, it's recommended to add your own introduction to your message, for
232             example "This is an automated call from ACME Inc...")
233              
234             =back
235              
236             If called in scalar context, returns 1 if the message was sent, 0 if it wasn't.
237              
238             If called in list context, returns a two-element list, the first element being
239             1 for success or 0 for fail, and the second being a message indicating why the
240             operation failed.
241              
242             Note that, at the current time, this feature supports only UK telephone numbers.
243              
244             =cut
245              
246             sub voice_push {
247              
248 0     0 1 0 my ($self, $to, $text, $opts) = @_;
249            
250 0 0       0 if (!$to) {
251 0         0 carp "SMS::AQL->voice_push() called without destination number";
252 0         0 return;
253             }
254            
255 0 0       0 if (!$text) {
256 0         0 carp "SMS::AQL->voice_push() called without message";
257 0         0 return;
258             }
259            
260             # voice push only works for UK numbers, and does not accept international
261             # format. If the number was given in +44 format, turn it into standard
262             # UK format; if it's an non-UK number, don't even try to send.
263 0         0 $to =~ s{^\+440?}{0};
264            
265 0 0       0 if ($to !~ m{^0}) {
266 0         0 carp "SMS::AQL->voice_push() called with a non-UK telephone number";
267 0         0 return;
268             }
269            
270 0         0 my %postdata = (
271             username => $self->{user},
272             password => $self->{pass},
273             msisdn => $to,
274             message => $text,
275             );
276            
277 0 0       0 if ($opts->{skipintro}) {
278 0         0 $postdata{skipintro} = 1;
279             }
280            
281            
282 0         0 my $response = $self->_do_post(
283             $self->{voice_servers}, '/voice_push.php', \%postdata
284             );
285            
286 0 0 0     0 if ($response && $response->is_success) {
287 0         0 my $status = (split /\n/, $response->content)[0];
288            
289 0         0 my %response_lookup = (
290             VP_OK => {
291             status => 1,
292             message => 'OK',
293             },
294             VP_ERR_NOTOMOBNUM => {
295             status => 0,
296             message => 'Telephone number not provided',
297             },
298             VP_ERR_INVALID_MOBNUM => {
299             status => 0,
300             message => 'Invalid telephone number',
301             },
302             VP_ERR_NOTGLOBAL => {
303             status => 0,
304             message => 'Voice push is currently only available for'
305             . ' UK telephone numbers',
306             },
307             VP_ERR_NOCREDIT => {
308             status => 0,
309             message => 'Insufficient credit',
310             },
311             VP_ERR_INVALIDAUTH => {
312             status => 0,
313             message => 'Username/password rejected',
314             },
315             VP_ERR_NOAUTH => {
316             # we should never see this, as we fail to create SMS::AQL
317             # instance without a username and password
318             status => 0,
319             message => 'Username/password not supplied',
320             },
321             VP_ERR_NOMSG => {
322             status => 0,
323             message => 'Message not provided',
324             },
325             );
326            
327 0         0 my $response_details = $response_lookup{$status};
328            
329 0 0       0 if (!$response_details) {
330 0         0 warn "Unrecognised status '$status' from AQL";
331 0         0 $response_details = {
332             status => 0,
333             message => 'Unrecognised response',
334             };
335             }
336            
337 0         0 $self->{last_response} = $status;
338 0         0 $self->{last_response_text} = $response_details->{message};
339 0         0 $self->{last_status} = $response_details->{status};
340            
341             return wantarray ?
342 0 0       0 @$response_details{qw(status message)} : $response_details->{status};
343            
344             } else {
345             # no response received:
346 0         0 $self->{last_response} = '';
347 0         0 $self->{last_response_text} = 'No response from AQL servers';
348 0         0 $self->{last_status} = 0;
349             return wantarray ?
350 0 0       0 (0, 'No response from AQL servers') : 0;
351             }
352              
353             }
354              
355              
356              
357             =item credit()
358              
359             Returns the current account credit. Returns undef if any errors occurred
360              
361             =cut
362              
363             sub credit {
364              
365 1     1 1 2 my $self = shift;
366              
367             # assemble the data we need to POST to the server:
368 1         4 my %postdata = (
369             'username' => $self->{user},
370             'password' => $self->{pass},
371             'cmd' => 'credit',
372             );
373            
374             # try the request to each sever in turn, stop as soon as one succeeds.
375 1         1 for my $server (sort { (-1,1)[rand 2] } @{$self->{sms_servers}} ) {
  0         0  
  1         5  
376            
377 1         6 my $response = $self->{ua}->post(
378             "http://$server/sms/postmsg.php", \%postdata);
379            
380 1 50       190536 next unless ($response->is_success); # try next server if we failed.
381            
382 1         18 $self->_check_aql_response_code($response);
383            
384 1         5 my ($credit) = $response->content =~ /AQSMS-CREDIT=(\d+)/;
385            
386 1         37 return $credit;
387            
388             }
389            
390 0         0 $self->_set_no_valid_response;
391 0         0 return undef;
392             } # end of sub credit
393              
394              
395              
396             =item last_status()
397              
398             Returns the status of the last command: 1 = OK, 0 = ERROR.
399              
400             =cut
401              
402 1     1 1 7 sub last_status { shift->{last_status} }
403              
404             =item last_error()
405              
406             Returns the error message of the last failed command.
407              
408             =cut
409              
410 0     0 1 0 sub last_error { shift->{last_error} }
411              
412             =item last_response()
413              
414             Returns the raw response from the AQL gateway.
415              
416             =cut
417              
418 0     0 1 0 sub last_response { shift->{last_response} }
419              
420             =item last_response_text()
421              
422             Returns the last result code received from the AQL
423             gateway in a readable format.
424              
425             Possible codes are:
426              
427             =over
428              
429             =item AQSMS-AUTHERROR
430              
431             The username and password supplied were incorrect
432              
433             =item AQSMS-NOCREDIT
434              
435             Out of credits (The account specified did not have sufficient credit)
436              
437             =item AQSMS-OK
438              
439             OK (The message was queued on our system successfully)
440              
441             =item AQSMS-NOMSG
442              
443             No message or no destination number were supplied
444              
445             =back
446              
447             =cut
448              
449             my %lookup = (
450             "AQSMS-AUTHERROR" => {
451             text => "The username and password supplied were incorrect",
452             status => 0,
453             },
454             "AQSMS-NOCREDIT" => {
455             #text => "The account specified did not have sufficient credit",
456             text => "Out of credits",
457             status => 0,
458             },
459             "AQSMS-OK" => {
460             #text => "The message was queued on our system successfully",
461             text => "OK",
462             status => 1,
463             },
464             "AQSMS-CREDIT" => {
465             #text is filled out in credit sub
466             status => 1,
467             },
468             "AQSMS-NOMSG" => {
469             text => "No message or no destination number were supplied",
470             status => 0,
471             },
472             "AQSMS-INVALID_DESTINATION" => {
473             text => "Invalid destination",
474             status => 0,
475             },
476             );
477              
478 0     0 1 0 sub last_response_text { shift->{last_response_text} }
479              
480              
481             # private implementation methods follow - you are advised not to call these
482             # directly, as their behaviour or even very existence could change in future
483             # versions.
484              
485             sub _check_aql_response_code {
486 1     1   4 my ($self, $res) = @_;
487 1         10 my $r = $self->{last_response} = $res->content;
488             # Strip everything after initial alphanumerics and hyphen:
489 1         21 $r =~ s/^([\w\-]+).*/$1/;
490 1 50       6 if (exists $lookup{$r}) {
491 1         5 $self->{last_response_text} = $lookup{$r}->{text};
492 1         5 $self->{last_status} = $lookup{$r}->{status};
493             } else {
494 0         0 $self->{last_response_text} = "$UNRECOGNISED_RESPONSE: $r";
495 0         0 $self->{last_status} = 0;
496             }
497 1 50       3 unless ($self->last_status) {
498 0           $self->{last_error} = $self->{last_response_text};
499             }
500             }
501              
502              
503              
504             # given an arrayref of possible servers, an URL and a hashref of POST data,
505             # makes a POST request to each server in turn, stopping as soon as a successful
506             # response is received and returning the LWP response object.
507             sub _do_post {
508              
509 0     0     my ($self, $servers, $url, $postdata) = @_;
510            
511 0 0         if (ref $servers ne 'ARRAY') {
512 0           die "_do_post expects an arrayref of servers to try";
513             }
514            
515 0 0         if (ref $postdata ne 'HASH') {
516 0           die "_do_post expects a hashref of post data";
517             }
518            
519 0 0 0       if (!$url || ref $url) {
520 0           die "_do_post expects an URL";
521             }
522            
523 0           $url =~ s{^/}{};
524            
525 0           for my $server (sort { (-1,1)[rand 2] } @{$servers} ) {
  0            
  0            
526 0           my $response = $self->{ua}->post(
527             "http://$server/$url", $postdata);
528            
529 0 0         if ($response->is_success) {
530 0           return $response;
531             }
532             }
533            
534             # if we get here, none of the servers we asked responded:
535 0           return;
536             }
537              
538              
539             # fix up the number
540             sub _canonical_number {
541              
542 0     0     my ($self, $num) = @_;
543            
544 0           $num =~ s/[^0-9+]//;
545 0 0         if (!$num) { return undef; }
  0            
546 0           $num =~ s/^0/+44/;
547            
548 0           return $num;
549             }
550              
551              
552             sub _set_no_valid_response {
553 0     0     my $self = shift;
554 0           $self->{last_error} = $NO_RESPONSES;
555 0           $self->{last_status} = 0;
556             }
557              
558              
559             1;
560             __END__