File Coverage

blib/lib/SMS/Send/Adapter/Node/Red.pm
Criterion Covered Total %
statement 56 94 59.5
branch 13 44 29.5
condition 2 13 15.3
subroutine 12 16 75.0
pod 10 10 100.0
total 93 177 52.5


line stmt bran cond sub pod time code
1             package SMS::Send::Adapter::Node::Red;
2 1     1   85526 use strict;
  1         4  
  1         29  
3 1     1   15 use warnings;
  1         5  
  1         27  
4 1     1   6 use base qw{Package::New};
  1         2  
  1         611  
5 1     1   250 use JSON qw{decode_json encode_json};
  1         2  
  1         9  
6 1     1   648 use SMS::Send;
  1         23457  
  1         33  
7 1     1   1069 use CGI;
  1         32074  
  1         7  
8              
9             our $VERSION = '0.05';
10              
11             =head1 NAME
12              
13             SMS::Send::Adapter::Node::Red - SMS::Send Adapter to Node-RED JSON HTTP request
14              
15             =head1 SYNOPSIS
16            
17             use SMS::Send::Adapter::Node::Red;
18             my $service = SMS::Send::Adapter::Node::Red->new(content => join('', <>));
19             $service->cgi_response;
20              
21             =head1 DESCRIPTION
22              
23             This Perl package provides an adapter from Node-RED HTTP request object with a JSON payload to the SMS::Send infrastructure using a CGI Apache script. The architecture works easiest with SMS::Send drivers based on the L base object since common settings can be stored in the configuration file.
24              
25             =head1 CONSTRUCTOR
26              
27             =head2 new
28              
29             my $object = SMS::Send::Adapter::Node::Red->new(content=>$string_of_json_object);
30              
31             =head1 PROPERTIES
32              
33             =head2 content
34              
35             JSON string payload of the HTTP post request.
36              
37             Example Payload:
38              
39             {
40             "to" : "7035551212",
41             "text" : "My Text Message",
42             "driver" : "VoIP::MS",
43             "options" : {}
44             }
45              
46             The Perl logic is based on this with lots of error trapping
47              
48             my $sent = SMS::Send->new($driver, %$options)->send_sms(to=>$to, text=>$text);
49              
50             I use a Node-RED function like this to format the JSON payload.
51              
52             my_text = msg.payload;
53             msg.payload = {
54             "driver" : "VoIP::MS",
55             "text" : my_text,
56             "to" : "7035551212",
57             "options" : {"key" : "value"},
58             };
59             return msg;
60              
61             =cut
62              
63             sub content {
64 2     2 1 4 my $self = shift;
65 2 50       7 die("Error: content not defined on construction") unless defined $self->{'content'};
66 2         14 return $self->{'content'};
67             }
68              
69             =head1 METHODS (STATE)
70              
71             =head2 input
72              
73             JSON Object from input that is passed to output.
74              
75             =cut
76              
77             sub input {
78 10     10 1 1781 my $self = shift;
79 10 100       27 unless ($self->{'input'}) {
80 2         3 local $@;
81 2         4 my $input = eval{decode_json($self->content)};
  2         7  
82 2         4 my $error = $@;
83 2 50       8 if ($error) {
    50          
84 0         0 $self->set_status_error(400=>'Error: JSON decode failed');
85 0         0 $self->{'input'} = {};
86             } elsif (ref($input) ne 'HASH') {
87 0         0 $self->set_status_error(400=>'Error: JSON Object required');
88 0         0 $self->{'input'} = {};
89             } else {
90 2         7 $self->{'input'} = $input;
91             }
92             }
93 10         45 return $self->{'input'};
94             }
95              
96             =head2 status
97              
98             HTTP Status Code returned to Node-RED is one of 200, 400, 500 or 502. Typically, a 200 means the SMS message was successfully sent to the provider, a 400 means a the input is misconfigured, a 500 means the server is misconfigured (verify installation), and a 502 means that the remote service is down or unauthorized.
99              
100             =cut
101              
102             sub status {
103 1     1 1 3 my $self = shift;
104 1 50       5 $self->{'status'} = shift if @_;
105 1 50       4 die("Error: status not set. sms_send method must be called first") unless $self->{'status'};
106 1         2 return $self->{'status'};
107             }
108              
109             =head2 status_string
110              
111             Format HTTP Status Code as string for web response
112              
113             =cut
114              
115             our $STATUS_STRING = {
116             200 => 'OK',
117             400 => 'Bad Request',
118             500 => 'Internal Server Error',
119             502 => 'Bad Gateway',
120             };
121              
122             sub status_string {
123 0     0 1 0 my $self = shift;
124 0         0 my $status = $self->status;
125 0 0       0 my $status_string = $STATUS_STRING->{$status} or die("Error: STATUS_STRING not defined for $status");
126 0         0 return "$status $status_string";
127             }
128              
129             =head2 error
130              
131             Error string passed in the JSON return object.
132              
133             =cut
134              
135             sub error {
136 1     1 1 2 my $self = shift;
137 1 50       6 $self->{'error'} = shift if @_;
138 1         2 return $self->{'error'};
139             }
140              
141             =head2 set_status_error
142              
143             Method to set the HTTP status and error with one function call.
144              
145             =cut
146              
147             sub set_status_error {
148 1     1 1 2 my $self = shift;
149 1 50       4 my $status = shift or die;
150 1   50     3 my $error = shift || '';
151 1         5 $self->status($status);
152 1         3 $self->error($error);
153 1         3 return $self;
154             }
155              
156             =head1 METHODS (ACTIONS)
157              
158             =head2 send_sms
159              
160             Wrapper around the SMS::Send->send_sms call.
161              
162             =cut
163              
164             sub send_sms {
165 0     0 1 0 my $self = shift;
166 0         0 my $sent = 0;
167 0         0 my $SMS = $self->SMS;
168 0 0       0 if ($SMS) {
169 0         0 my $to = $self->input->{'to'};
170 0         0 my $text = $self->input->{'text'};
171 0 0 0     0 if ($to and $text) {
    0 0        
    0 0        
172 0         0 local $@;
173 0         0 $sent = eval{$SMS->send_sms(to=>$to, text=>$text)};
  0         0  
174 0         0 my $error = $@;
175 0 0       0 if ($error) {
    0          
176 0         0 $self->set_status_error(502=>"Error: Failed call SMS::Send->send_sms. $error");
177             } elsif (!$sent) {
178 0         0 $self->set_status_error(502=>'Error: Unknown. SMS::Send->send_sms returned unsuccessful');
179             } else {
180 0         0 $self->set_status_error(200=>'');
181             }
182             } elsif (!$to and $text) {
183 0         0 $self->set_status_error(400=>'Error: JSON input missing "to"');
184             } elsif ($to and !$text) {
185 0         0 $self->set_status_error(400=>'Error: JSON input missing "text"');
186             } else {
187 0         0 $self->set_status_error(400=>'Error: JSON input missing "to" and "text"');
188             }
189             }
190 0         0 return $sent;
191             }
192              
193             =head2 cgi_response
194              
195             Formatted CGI response
196              
197             =cut
198              
199             sub cgi_response {
200 0     0 1 0 my $self = shift;
201 0 0       0 my $sent = $self->send_sms ? \1 : \0; #sets object properties
202 0         0 my %response = (sent => $sent);
203 0 0       0 $response{'error'} = $self->error if $self->error;
204 0 0       0 $response{'input'} = $self->input if $self->input;
205 0         0 print $self->CGI->header(
206             -status => $self->status_string,
207             -type => 'application/json',
208             ),
209             encode_json(\%response),
210             "\n";
211             }
212              
213             =head1 OBJECT ACCESSORS
214              
215             =head2 CGI
216              
217             Returns a L object for use in this package.
218              
219             =cut
220              
221             sub CGI {
222 0     0 1 0 my $self = shift;
223 0 0       0 $self->{'CGI'} = CGI->new('') unless $self->{'CGI'};
224 0         0 return $self->{'CGI'};
225             }
226              
227             =head2 SMS
228              
229             Returns a L object for use in this package.
230              
231             =cut
232              
233             sub SMS {
234 2     2 1 6 my $self = shift;
235 2         5 my $driver = $self->input->{'driver'};
236 2 50       6 if ($driver) {
237 2   50     5 my $options = $self->input->{'options'} || {};
238 2 50       8 if (ref($options) eq 'HASH') {
239 2         4 local $@;
240 2         3 $self->{'SMS'} = eval{SMS::Send->new($driver, %$options)};
  2         13  
241 2         799 my $error = $@;
242 2 100       29 if ($error) {
243 1         8 my $text = qq{Failed to load Perl package SMS::Send with driver "$driver". Please ensure both SMS::Send and SMS::Send::$driver are installed. $error};
244 1         5 $self->set_status_error(500=>$text);
245             }
246             } else {
247 0         0 $self->set_status_error(400=>'Error: JSON input "options" not an object.');
248             }
249             } else {
250 0         0 $self->set_status_error(400=>'Error: JSON input missing "driver".');
251             }
252 2         9 return $self->{'SMS'};
253             }
254              
255             =head1 SEE ALSO
256              
257             L
258              
259             =head1 AUTHOR
260              
261             Michael R. Davis
262              
263             =head1 COPYRIGHT AND LICENSE
264              
265             MIT License
266            
267             Copyright (c) 2020 Michael R. Davis
268            
269             Permission is hereby granted, free of charge, to any person obtaining a copy
270             of this software and associated documentation files (the "Software"), to deal
271             in the Software without restriction, including without limitation the rights
272             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
273             copies of the Software, and to permit persons to whom the Software is
274             furnished to do so, subject to the following conditions:
275            
276             The above copyright notice and this permission notice shall be included in all
277             copies or substantial portions of the Software.
278            
279             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
280             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
281             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
282             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
283             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
284             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
285             SOFTWARE.
286              
287             =cut
288              
289             1;