File Coverage

blib/lib/Net/SMS/TextMagic.pm
Criterion Covered Total %
statement 35 175 20.0
branch 5 46 10.8
condition 3 12 25.0
subroutine 7 19 36.8
pod 13 13 100.0
total 63 265 23.7


line stmt bran cond sub pod time code
1             #
2             # Net::SMS::TextMagic This module provides access to TextMagic SMS service
3             #
4             # Author: Matti Lattu
5             #
6             # Copyright (c) 2011 Matti Lattu. All rights reserved.
7             # This program is free software; you can redistribute it and/or
8             # modify it under the same terms as Perl itself.
9             #
10             # For TextMagic and its possible Copyright see http://www.textmagic.com/
11              
12             package Net::SMS::TextMagic;
13              
14 1     1   34149 use strict;
  1         3  
  1         56  
15 1     1   5 use warnings;
  1         2  
  1         47  
16              
17             our $VERSION = '1.00';
18              
19 1     1   1209 use JSON;
  1         16849  
  1         6  
20 1     1   1454 use LWP::UserAgent;
  1         50815  
  1         41  
21 1     1   12 use URI::Escape qw(uri_escape uri_escape_utf8);
  1         2  
  1         87  
22 1     1   1075 use Encode qw(encode decode);
  1         11758  
  1         1733  
23              
24             =encoding utf8
25              
26             =head1 SYNOPSIS
27              
28             use Net::SMS::TextMagic;
29              
30             my $sms = Net::SMS::TextMagic->new( USER => $username, API_ID => $api_id );
31             $sms->message_send(text=>'This is a test!', phone=>'35891911');
32              
33             =head1 DESCRIPTION
34              
35             TextMagic (http://www.textmagic.com) is a commercial service that allows its users to send
36             SMS messages to anyone in the world.
37              
38             Net::SMS::TextMagic provides OO methods that allow to send SMS messages through
39             TextMagic service. The TextMagic HTTPS API is documented at
40             L. This module implements all HTTPS API commands
41             and return values.
42             However, all command parameters and return values are not necessary mentioned or thorougly
43             described in this document. Please consult the original API document for details.
44              
45             Note that whether this software nor the author are related to TextMagic in any way.
46              
47             =head1 METHODS
48              
49             =over 4
50              
51             =item new
52              
53             Creates the TextMagic object.
54              
55             Usage:
56              
57             my $sms = Net::SMS::TextMagic->new( USER => $username, API_ID => $api_id );
58              
59             The complete list of arguments is:
60              
61             B
62             Your TextMagic username. Same as your username to TextMagic web service.
63             Required.
64              
65             B
66             Your TextMagic API ID. The API ID is not your password to TextMagic
67             web service. You can get your API ID from the
68             TextMagic web service. Required.
69              
70             B
71             TextMagic API URL. Defaults to https://www.textmagic.com/app/api.
72              
73             B
74             Name of the user agent you want to display to TextMagic service.
75             Defaults to "Net::SMS::TextMagic"
76              
77             B
78             UserAgent timeout. Defaults to 10 seconds.
79              
80             Returns Net::SMS::TextMagic object or false (in case USER or API_ID are missing).
81              
82             =cut
83              
84             sub new {
85 1     1 1 14 my ($class, %params) = @_;
86            
87 1         3 my $self = {};
88              
89             # Store TextMagic username (required)
90            
91 1 50       5 if ($params{'USER'}) {
92 1         4 $self->{'USER'} = $params{'USER'};
93             }
94             else {
95             # USER parameter is missing - return false
96 0         0 return;
97             }
98            
99             # Store TextMagic API ID (required)
100            
101 1 50       4 if ($params{'API_ID'}) {
102 1         4 $self->{'API_ID'} = $params{'API_ID'};
103             }
104             else {
105             # API_ID parameter is missing - return false
106 0         0 return;
107             }
108              
109             # Create LWP::UserAgent
110 1         10 $self->{'UA'} = LWP::UserAgent->new;
111            
112             # Set TextMagic API URL (optional)
113            
114 1 50 33     3735 if (defined($params{'API_URL'}) and $params{'API_URL'} ne '') {
115 0         0 $self->{'API_URL'} = $params{'API_URL'};
116             }
117             else {
118 1         4 $self->{'API_URL'} = 'https://www.textmagic.com/app/api';
119             }
120              
121             # Set UserAgent string (optional)
122            
123 1 50 33     5 if (defined($params{'UserAgent'}) and $params{'UserAgent'} ne '') {
124 0         0 $self->{'UA'}->agent($params{'UserAgent'});
125             }
126             else {
127 1         6 $self->{'UA'}->agent('Net::SMS::TextMagic');
128             }
129            
130             # Set UserAgent timeout (optional)
131            
132 1 50 33     70 if (defined($params{'Timeout'}) and $params{'Timeout'} ne '') {
133 0         0 $self->{'UA'}->timeout($params{'Timeout'});
134             }
135             else {
136             # Default timeout is 10 seconds
137            
138 1         7 $self->{'UA'}->timeout(10);
139             }
140              
141             # Current error message
142 1         19 $self->{'ERROR'} = undef;
143            
144             # Unprocessed error messages
145 1         4 $self->{'ERROR_UNPROC'} = [];
146            
147 1         4 bless($self, $class);
148              
149 1         6 return $self;
150             }
151              
152             =item message_send
153              
154             Sends SMS message. Equals to HTTPS API command "send" but renamed in
155             the module to avoid conflict with built-in send function.
156              
157             Usage:
158              
159             my %response = $sms->message_send(text=>'This is a test!', phone=>'35891911');
160             print "Sent message $response{'sent_text'} chopped in $response{'parts_count'} parts\n";
161              
162             The complete list of arguments is:
163              
164             B
165             Message text in Perl internal charset. Required.
166              
167             B
168             Phone number(s). The numbers are sent "as is". Currently the API supports up to
169             100 numbers separated by commas (,). Required.
170              
171             B
172             If set to true the message text is sent as UTF-8. Otherwise the text is sent
173             in GSM 03.38 encoding. Defaults to true (UTF-8).
174              
175             B, B, B
176             See TextMagic HTTPS API documentation.
177              
178             Returns a hash containing following keys:
179              
180             B
181             The text (in Perl internal charset) that was actually sent.
182              
183             B
184             The number of parts the message has.
185              
186             B
187             A hash containing pairs of numbers (keys) and message_ids (values) that were
188             sent.
189              
190             =cut
191              
192             sub message_send {
193 0     0 1   my ($class, %params) = @_;
194            
195             # Check for required parameters
196 0 0 0       if ((!$params{'text'}) or (!$params{'phone'})) {
197 0           set_error($class, "method 'message_send' was called without required parameters");
198 0           return;
199             }
200              
201             # UNICODE defaults to true
202 0 0         if (!defined($params{'unicode'})) {
203 0           $params{'unicode'} = 1;
204             }
205            
206 0 0         if ($params{'unicode'}) {
207             # Encode message as UTF8
208 0           $params{'text'} = uri_escape($params{'text'});
209             }
210             else {
211             # Encode message as GSM 03.38
212 0           $params{'text'} = encode("gsm0338", $params{'text'});
213 0           $params{'text'} = uri_escape($params{'text'});
214             }
215            
216 0           my $r_json = contact_api($class, 'send', %params);
217            
218 0 0         if (defined($r_json)) {
219             # No errors
220            
221 0           my %response = ();
222              
223 0           $response{'sent_text'} = $r_json->{'sent_text'};
224              
225 0           $response{'parts_count'} = $r_json->{'parts_count'};
226              
227 0           while (my ($id, $number) = each %{$r_json->{"message_id"}} ) {
  0            
228 0           $response{'message_id'}{$number} = $id;
229             }
230              
231 0           return %response;
232             }
233             else {
234             # Errors, we expect that error flag was raised in contact_api()
235            
236 0           return;
237             }
238             }
239              
240             =item account
241              
242             Get the current SMS credit balance.
243              
244             Usage:
245              
246             my %response = $sms->account();
247             print "Your balance is $response{'balance'}\n";
248            
249             Returns a hash containing following key:
250              
251             B
252             The amount of available SMS credits on your account.
253              
254             =cut
255              
256             sub account {
257 0     0 1   my ($class) = @_;
258            
259 0           my $r_json = contact_api($class, 'account');
260            
261 0 0         if (defined($r_json)) {
262             # No errors
263            
264 0           my %response = ();
265            
266 0           $response{'balance'} = $r_json->{'balance'};
267            
268 0           return %response;
269             }
270             else {
271             # Errors, we expect that error flag was raised in contact_api()
272            
273 0           return;
274             }
275             }
276            
277             =item message_status
278              
279             This method allows you to retrieve the delivery status of any SMS you have
280             already sent. The message ID is returned by message_send command.
281              
282             Usage:
283              
284             my $this_id = 123456;
285             my %response = $sms->message_status($this_id);
286             print 'Message text was '.$response{$this_id}{'text'}."\n";
287             print 'The cost was '.$response{$this_id}{'credits_cost'}." credits\n";
288              
289             The only parameter is a string containing message ID or several IDs
290             separated by commas and without spaces (e.g. "8624389,8624390,8624391").
291             Up to 100 IDs can be retrieved with a single command.
292              
293             Returns a two-level hash containing status of message IDs. Each ID has following
294             fields:
295              
296             B
297             SMS text sent.
298              
299             B
300             The current status of the message. The status codes are explained at
301             L.
302              
303             B
304             The time TextMagic sent the message. Unix timestamp.
305              
306             B
307             See API documentation for details.
308              
309             B
310             Cost of the message in SMS credits. Set when message is delivered.
311              
312             B
313             The time your message achieves final status, returned by the mobile operator.
314             Unix timestamp.
315              
316             =cut
317              
318             sub message_status {
319 0     0 1   my ($class, $ids) = @_;
320            
321             # Check for required parameters
322 0 0         if (!$ids) {
323 0           set_error($class, "method 'message_status' was called without parameters");
324 0           return;
325             }
326              
327 0           my %params = ('ids' => $ids);
328            
329 0           my $r_json = contact_api($class, 'message_status', %params);
330            
331 0 0         if (defined($r_json)) {
332             # No errors
333            
334 0           my %response = ();
335              
336 0           foreach my $this_id (keys %{$r_json}) {
  0            
337 0           $response{$this_id} = ();
338 0           $response{$this_id}{'text'} = encode("utf8", $r_json->{$this_id}->{'text'});
339 0           $response{$this_id}{'status'} = $r_json->{$this_id}->{'status'};
340 0           $response{$this_id}{'created_time'} = $r_json->{$this_id}->{'created_time'};
341 0           $response{$this_id}{'reply_number'} = $r_json->{$this_id}->{'reply_number'};
342 0           $response{$this_id}{'credits_cost'} = $r_json->{$this_id}->{'credits_cost'};
343 0           $response{$this_id}{'completed_time'} = $r_json->{$this_id}->{'completed_time'};
344             }
345              
346 0           return %response;
347             }
348             else {
349             # Errors, we expect that error flag was raised in contact_api()
350            
351 0           return;
352             }
353             }
354              
355             =item receive
356              
357             This method retrieves the incoming SMS messages from the server.
358             The server is limited to returning a maximum of 100 messages for
359             each request. Please use last_retrieved_id parameter to page through
360             your inbox.
361              
362             The only optional parameter is B. The server will
363             only return messages with identifiers greater than B.
364             The default value is 0 which fetches up to the first 100 replies from
365             your inbox.
366              
367             Returns a hash containing following keys:
368              
369             B
370             The number of messages with identifiers greater than B
371             remaining unreturned due to the limit on returned messages per request.
372              
373             B
374             Number of messages in the current B hash.
375              
376             B
377             An array containing all messages. Each object in the array is a hash
378             with following keys:
379              
380             =over 4
381              
382             B
383             The identifier of the incoming message.
384              
385             B
386             The sender's phone number.
387              
388             B
389             The message's reception time expressed in Unix time format.
390              
391             B
392             The message text.
393              
394             =back
395              
396             =cut
397              
398             sub receive {
399 0     0 1   my ($class,$lrid) = @_;
400            
401 0 0         if (!defined($lrid)) {
402             # Default value for last_retrieved_id
403 0           $lrid = 0;
404             }
405              
406 0           my %params = ('last_retrieved_id' => $lrid);
407            
408 0           my $r_json = contact_api($class, 'receive', %params);
409            
410 0 0         if (defined($r_json)) {
411             # No errors
412            
413 0           my %response = ();
414              
415 0           $response{'unread'} = $r_json->{'unread'};
416 0           $response{'messages_count'} = scalar(@{$r_json->{'messages'}});
  0            
417              
418 0           print "messages count: ".scalar(@{$r_json->{'messages'}})."\n";
  0            
419            
420 0           for (my $i=0; $i < scalar(@{$r_json->{'messages'}}); $i++) {
  0            
421 0           my %this_msg_data = ();
422 0           $this_msg_data{'message_id'} = @{$r_json->{'messages'}}[$i]->{'message_id'};
  0            
423 0           $this_msg_data{'from'} = @{$r_json->{'messages'}}[$i]->{'from'};
  0            
424 0           $this_msg_data{'timestamp'} = @{$r_json->{'messages'}}[$i]->{'timestamp'};
  0            
425 0           $this_msg_data{'text'} = encode("utf8", @{$r_json->{'messages'}}[$i]->{'text'});
  0            
426 0           push(@{$response{'messages'}}, {%this_msg_data});
  0            
427             }
428              
429 0           return %response;
430             }
431             else {
432             # Errors, we expect that error flag was raised in contact_api()
433            
434 0           return;
435             }
436             }
437              
438             =item delete_reply
439              
440             This command helps you to delete any incoming SMS messages from the server.
441              
442             Usage:
443              
444             my @response = $sms->delete_reply('123456,123457,123458');
445             print "Following messages were deleted: ".join(', ', @response)."\n";
446              
447             The only required parameter is a string containing message IDs to be deleted.
448             The IDs should be separated with commas without spaces. Up to 100 messages
449             can be deleted with single command.
450              
451             Returns an array containing message IDs that were deleted.
452              
453             =cut
454              
455             sub delete_reply {
456 0     0 1   my ($class, $delids) = @_;
457            
458 0 0         if (!defined($delids)) {
459 0           set_error($class, "method 'delete_reply' was called without parameters");
460 0           return;
461             }
462              
463 0           my %params = ('ids' => $delids);
464            
465 0           my $r_json = contact_api($class, 'delete_reply', %params);
466            
467 0 0         if (defined($r_json)) {
468             # No errors
469            
470 0           return @{$r_json->{'deleted'}};
  0            
471             }
472             else {
473             # Errors, we expect that error flag was raised in contact_api()
474            
475 0           return;
476             }
477             }
478              
479             =item check_number
480              
481             This command helps you to validate a phone number's format and to check a
482             message's price to its destination.
483              
484             Usage:
485              
486             my %response = $sms->check_number('35891911,35891912');
487            
488             foreach my $this_number (keys %response) {
489             print "Number $this_number is in ".
490             $response{$this_number}{'country'}.
491             'and the SMS cost is '.
492             $response{$this_number}{'price'}.
493             "credits.\n";
494             }
495              
496             The only required parameter is a string containing phone numbers to be checked.
497             The numbers should be separated with commas without spaces. The TextMagic HTTPS
498             API documentation does not specify the maximum number of phone numbers that
499             can be checked with a single command.
500              
501             Returns a two-level hash containing status of numbers. Each number has following
502             fields:
503              
504             B
505             The cost in SMS credits of sending a single message to the number.
506              
507             B
508             The number's country code. A full list of country codes can be found at
509             L.
510              
511             =cut
512              
513             sub check_number {
514 0     0 1   my ($class, $numbers) = @_;
515            
516 0 0         if (!defined($numbers)) {
517 0           set_error($class, "method 'check_number' was called without parameters");
518 0           return;
519             }
520              
521 0           my %params = ('phone' => $numbers);
522            
523 0           my $r_json = contact_api($class, 'check_number', %params);
524            
525 0 0         if (defined($r_json)) {
526             # No errors
527            
528 0           my %response = ();
529            
530 0           foreach my $this_number (keys %{$r_json}) {
  0            
531 0           $response{$this_number}{'price'} = $r_json->{$this_number}->{'price'};
532 0           $response{$this_number}{'country'} = $r_json->{$this_number}->{'country'};
533             }
534              
535 0           return %response;
536             }
537             else {
538             # Errors, we expect that error flag was raised in contact_api()
539            
540 0           return;
541             }
542             }
543              
544             =item contact_api
545              
546             Contacts TextMagic API. This in mainly for internal use, but can be used
547             to contact TextMagic API directly.
548              
549             Usage:
550              
551             my $r_json = $sms->contact_api('some_api_command', %parameters);
552              
553             Parameters:
554             - a string containing TextMagic HTTPS API command
555             - a hash containing command parameters
556              
557             Returns a JSON object containing the result.
558              
559             =cut
560              
561             sub contact_api {
562 0     0 1   my ($class, $cmd, %params) = @_;
563            
564 0           $params{'cmd'} = $cmd;
565 0           $params{'username'} = $class->{'USER'};
566 0           $params{'password'} = $class->{'API_ID'};
567            
568 0           my $response = $class->{'UA'}->post($class->{'API_URL'}, Content=>\%params);
569            
570 0 0         if ($response->is_success) {
571             # For debugging
572             # print "---JSON begins:\n".$response->decoded_content."\n---JSON ends\n";
573 0           my $json_obj = decode_json($response->decoded_content);
574 0 0         if ($json_obj->{'error_code'}) {
575             # API error, set error message
576            
577 0           set_error($class, 'API error #'.$json_obj->{'error_code'}.': '.$json_obj->{'error_message'});
578 0           return;
579             }
580             else {
581             # API ok, return JSON object
582            
583 0           return $json_obj;
584             }
585             }
586             else {
587             # HTTP POST failed
588 0           set_error($class, "HTTP POST failed: ".$response->status_line);
589 0           return;
590             }
591             }
592              
593             =item set_error
594              
595             Sets Net::SMS::TextMagic error code. Mainly for internal use.
596              
597             If there is already an unprocessed error message, this message
598             is appended to the unprocessed error array. The array can be read
599             and emptied with get_unprocessed_errors().
600              
601             Usage:
602              
603             $sms->set_error('Out of credit');
604              
605             =cut
606              
607             sub set_error {
608 0     0 1   my ($class, $errormsg) = @_;
609            
610             # If there are alredy current error, add error to error buffer
611 0 0         if ($class->{'ERROR'}) {
612 0           push(@{$class->{'ERROR_UNPROC'}}, $class->{'ERROR'});
  0            
613             }
614            
615 0           $class->{'ERROR'} = $errormsg;
616            
617 0           return 1;
618             }
619              
620             =item get_error
621              
622             Gets and clears Net::SMS::TextMagic error code. This does not affect the
623             array of unprocessed error messages (see get_unprocessed_errors()).
624              
625             Usage:
626              
627             my %response = $sms->message_send('phone' => '123456', 'text' => 'Howdy!');
628             if ($sms->if_error()) {
629             my $errormsg = $sms->get_error();
630             if ($errormsg) {
631             print "Dough! $errormsg\n";
632             }
633             }
634              
635             No parameters. Returns a error string if error flag is up. If no error message is
636             present returns undef.
637              
638             =cut
639              
640             sub get_error {
641 0     0 1   my ($class) = @_;
642            
643 0           my $errormsg = $class->{'ERROR'};
644 0           $class->{'ERROR'} = undef;
645            
646 0           return $errormsg;
647             }
648              
649             =item if_error
650              
651             Returns true if there is a pending error code (see get_error()).
652             Returns false if no error flag is set.
653              
654             Usage:
655              
656             if ($sms->if_error()) {
657             print STDERR "SMS error: ".$sms->get_error()."\n";
658             }
659            
660             =cut
661              
662             sub if_error {
663 0     0 1   my ($class) = @_;
664            
665 0 0         if ($class->{'ERROR'}) {
666 0           return 1;
667             }
668             else {
669 0           return;
670             }
671             }
672              
673             =item get_unprocessed_errors
674              
675             Returns and flushes the array of unprocessed errors. See set_error().
676              
677             =cut
678              
679             sub get_unprocessed_errors {
680 0     0 1   my ($class) = @_;
681            
682 0           my @errors = @{$class->{'ERRORS_UNPROC'}};
  0            
683            
684 0           $class->{'ERRORS_UNPROC'} = [];
685            
686 0           return @errors;
687             }
688              
689             =item if_unprocessed_errors
690              
691             Returns a number of items in the array of unprocessed errors. If
692             there are no errors returns undef (false).
693              
694             =back
695              
696             =cut
697              
698             sub if_unprocessed_errors {
699 0     0 1   my ($class) = @_;
700            
701 0           my $n = scalar(@{$class->{'ERRORS_UNPROC'}});
  0            
702            
703 0 0         if ($n == 0) {
704 0           return;
705             }
706            
707 0           return $n;
708             }
709            
710             1;
711              
712             __END__