File Coverage

blib/lib/SMS/Send/Adapter/Node/Red.pm
Criterion Covered Total %
statement 74 123 60.1
branch 23 66 34.8
condition 3 18 16.6
subroutine 15 19 78.9
pod 12 12 100.0
total 127 238 53.3


line stmt bran cond sub pod time code
1             package SMS::Send::Adapter::Node::Red;
2 2     2   456294 use strict;
  2         7  
  2         75  
3 2     2   9 use warnings;
  2         3  
  2         126  
4 2     2   13 use base qw{Package::New};
  2         3  
  2         1047  
5 2     2   1434 use JSON::XS qw{decode_json encode_json};
  2         6697  
  2         132  
6 2     2   951 use SMS::Send;
  2         48345  
  2         139  
7 2     2   2106 use CGI;
  2         88140  
  2         12  
8              
9             our $VERSION = '0.10';
10             our $PACKAGE = __PACKAGE__;
11              
12             =head1 NAME
13              
14             SMS::Send::Adapter::Node::Red - SMS::Send Adapter to Node-RED JSON HTTP request
15              
16             =head1 SYNOPSIS
17              
18             CGI Application
19              
20             use SMS::Send::Adapter::Node::Red;
21             my $service = SMS::Send::Adapter::Node::Red->new(content => join('', <>));
22             $service->cgi_response;
23              
24             PSGI Application
25              
26             use SMS::Send::Adapter::Node::Red;
27             SMS::Send::Adapter::Node::Red->psgi_app
28              
29             PSGI Plack Mount
30              
31             use SMS::Send::Adapter::Node::Red;
32             use Plack::Builder qw{builder mount};
33             builder {
34             mount '/sms' => SMS::Send::Adapter::Node::Red->psgi_app;
35             mount '/' => sub {[404=> [], []]};
36             }
37              
38             =head1 DESCRIPTION
39              
40             This Perl package provides an adapter from Node-RED HTTP request object with a JSON payload to the SMS::Send infrastructure using either a PSGI or a CGI 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.
41              
42             =head1 CONSTRUCTOR
43              
44             =head2 new
45              
46             my $object = SMS::Send::Adapter::Node::Red->new(content=>$string_of_json_object);
47              
48             =head1 PROPERTIES
49              
50             =head2 content
51              
52             JSON string payload of the HTTP post request.
53              
54             Example Payload:
55              
56             {
57             "to" : "7035551212",
58             "text" : "My Text Message",
59             "driver" : "VoIP::MS",
60             "options" : {}
61             }
62              
63             The Perl logic is based on this one-liner with lots of error trapping
64              
65             my $sent = SMS::Send->new($driver, %$options)->send_sms(to=>$to, text=>$text);
66              
67             I use a Node-RED function like this to format the JSON payload.
68              
69             my_text = msg.payload;
70             msg.payload = {
71             "driver" : "VoIP::MS",
72             "text" : my_text,
73             "to" : "7035551212",
74             "options" : {"key" : "value"},
75             };
76             return msg;
77              
78             =cut
79              
80             sub content {
81 4     4 1 9 my $self = shift;
82 4 50       12 die("Error: content not defined on construction") unless defined $self->{'content'};
83 4         88 return $self->{'content'};
84             }
85              
86             =head1 METHODS (STATE)
87              
88             =head2 input
89              
90             JSON Object from input that is passed to output.
91              
92             =cut
93              
94             sub input {
95 14     14 1 431875 my $self = shift;
96 14 100       90 if (not defined $self->{'input'}) {
97 4         9 local $@;
98 4         11 my $input = eval{decode_json($self->content)};
  4         17  
99 4         10 my $error = $@;
100 4 100       18 if ($error) {
    100          
101 1         5 $self->set_status_error(400=>'Error: JSON decode failed');
102             } elsif (ref($input) ne 'HASH') {
103 1         4 $self->set_status_error(400=>'Error: JSON Object required');
104             } else {
105 2         8 $self->{'input'} = $input;
106             }
107             }
108 14         69 return $self->{'input'};
109             }
110              
111             =head2 status
112              
113             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 the input is malformed, a 500 means the server is misconfigured (verify installation), and a 502 means that the remote service has issues or is unreachable.
114              
115             =cut
116              
117             sub status {
118 9     9 1 21 my $self = shift;
119 9 100       31 $self->{'status'} = shift if @_;
120 9 50       29 die("Error: status not set. sms_send method must be called first") unless $self->{'status'};
121 9         29 return $self->{'status'};
122             }
123              
124             =head2 status_string
125              
126             Format HTTP Status Code as string for web response
127              
128             =cut
129              
130             our $STATUS_STRING = {
131             200 => 'OK',
132             400 => 'Bad Request',
133             500 => 'Internal Server Error',
134             502 => 'Bad Gateway',
135             };
136              
137             sub status_string {
138 3     3 1 8 my $self = shift;
139 3         11 my $status = $self->status;
140 3 50       14 my $status_string = $STATUS_STRING->{$status} or die("Error: STATUS_STRING not defined for $status");
141 3         24 return "$status $status_string";
142             }
143              
144             =head2 error
145              
146             Error string passed in the JSON return object.
147              
148             =cut
149              
150             sub error {
151 7     7 1 36 my $self = shift;
152 7 100       26 $self->{'error'} = shift if @_;
153 7         31 return $self->{'error'};
154             }
155              
156             =head2 set_status_error
157              
158             Method to set the HTTP status and error with one function call.
159              
160             =cut
161              
162             sub set_status_error {
163 3     3 1 6 my $self = shift;
164 3 50       11 my $status = shift or die;
165 3   50     10 my $error = shift || '';
166 3         30 $self->status($status);
167 3         13 $self->error($error);
168 3         9 return $self;
169             }
170              
171             =head2 driver
172              
173             Returns configured SMS driver from input, environment, or SMS-Send.ini.
174              
175             =cut
176              
177             sub driver {
178 2     2 1 3 my $self = shift;
179 2         5 my $driver = undef;
180 2         4 my $ini_file = '/etc/SMS-Send.ini';
181 2         6 my $input = $self->input; #undef on error
182 2 50       7 if (defined $input) {
183             #Set driver from input
184 2 50       8 $driver = $input->{'driver'} if $input->{'driver'};
185             }
186 2 50       7 if (!$driver) {
187             #Set driver from environment
188 0         0 my $DRIVER = $ENV{'SMS_SEND_ADAPTER_NODE_RED_DRIVER'};
189 0 0       0 $driver = $DRIVER if $DRIVER;
190             }
191 2 50 33     8 if (!$driver and -r $ini_file) {
192             #Set driver from INI file
193 0         0 require Config::IniFiles;
194 0         0 my $cfg = Config::IniFiles->new(-file=>$ini_file);
195 0         0 my @drivers = grep {$cfg->val($_, 'active', '1')} $cfg->Sections;
  0         0  
196 0 0       0 $driver = $drivers[0] if @drivers;
197             }
198 2         6 return $driver;
199             }
200              
201             =head1 METHODS (ACTIONS)
202              
203             =head2 send_sms
204              
205             Wrapper around the SMS::Send->send_sms call.
206              
207             =cut
208              
209             sub send_sms {
210 0     0 1 0 my $self = shift;
211 0         0 my $sent = 0;
212 0         0 my $SMS = $self->SMS;
213 0 0       0 if ($SMS) {
214 0         0 my $to = $self->input->{'to'};
215 0         0 my $text = $self->input->{'text'};
216 0 0 0     0 if ($to and $text) {
    0 0        
    0 0        
217 0         0 local $@;
218 0         0 $sent = eval{$SMS->send_sms(to=>$to, text=>$text)};
  0         0  
219 0         0 my $error = $@;
220 0 0       0 if ($error) {
    0          
221 0         0 $self->set_status_error(502=>"Error: Failed call SMS::Send->send_sms. $error");
222             } elsif (!$sent) {
223 0         0 $self->set_status_error(502=>'Error: Unknown. SMS::Send->send_sms returned unsuccessful');
224             } else {
225 0         0 $self->set_status_error(200=>'');
226             }
227             } elsif (!$to and $text) {
228 0         0 $self->set_status_error(400=>'Error: JSON input missing "to"');
229             } elsif ($to and !$text) {
230 0         0 $self->set_status_error(400=>'Error: JSON input missing "text"');
231             } else {
232 0         0 $self->set_status_error(400=>'Error: JSON input missing "to" and "text"');
233             }
234             }
235 0         0 return $sent;
236             }
237              
238             =head2 cgi_response
239              
240             Formatted CGI response
241              
242             =cut
243              
244             sub cgi_response {
245 0     0 1 0 my $self = shift;
246 0 0       0 my $sent = $self->send_sms ? \1 : \0; #sets object properties
247 0         0 my %response = (sent => $sent);
248 0 0       0 $response{'error'} = $self->error if $self->error;
249 0 0       0 $response{'input'} = $self->input if $self->input;
250 0         0 print $self->CGI->header(
251             -status => $self->status_string,
252             -type => 'application/json',
253             ),
254             encode_json(\%response),
255             "\n";
256             }
257              
258             =head2 psgi_app
259              
260             Returns a PSGI application
261              
262             =cut
263              
264             sub psgi_app {
265             return sub {
266 0     0   0 my $env = shift;
267 0   0     0 my $length = $env->{'CONTENT_LENGTH'} || 0;
268 0         0 my $content = '';
269 0 0       0 if ($length > 0) {
270 0         0 my $fh = $env->{'psgi.input'};
271 0         0 $fh->read($content, $length, 0);
272             }
273 0         0 my $service = $PACKAGE->new(content => $content);
274 0 0       0 my $sent = $service->send_sms ? \1 : \0; #sets object properties
275 0         0 my %response = (sent => $sent);
276 0 0       0 $response{'error'} = $service->error if $service->error;
277 0 0       0 $response{'input'} = $service->input if $service->input;
278              
279             return [
280 0         0 $service->status,
281             [ 'Content-Type' => 'application/json' ],
282             [ encode_json(\%response), "\n" ],
283             ];
284 1     1 1 446 };
285             }
286              
287             =head1 OBJECT ACCESSORS
288              
289             =head2 CGI
290              
291             Returns a L object for use in this package.
292              
293             =cut
294              
295             sub CGI {
296 0     0 1 0 my $self = shift;
297 0 0       0 $self->{'CGI'} = CGI->new('') unless $self->{'CGI'};
298 0         0 return $self->{'CGI'};
299             }
300              
301             =head2 SMS
302              
303             Returns a L object for use in this package.
304              
305             =cut
306              
307             sub SMS {
308 2     2 1 5 my $self = shift;
309 2         8 my $input = $self->input; #undef on error
310 2 50       8 if (defined $input) {
311 2         9 my $driver = $self->driver;
312 2 50       7 if ($driver) {
313 2   50     13 my $options = $input->{'options'} || {};
314 2 50       9 if (ref($options) eq 'HASH') {
315 2         3 local $@;
316 2         5 $self->{'SMS'} = eval{SMS::Send->new($driver, %$options)};
  2         16  
317 2         964 my $error = $@;
318 2 100       11 if ($error) {
319 1         12 my $text = qq{Failed to load Perl package SMS::Send with driver "$driver". Ensure SMS::Send::$driver is installed. $error};
320 1         4 $self->set_status_error(500=>$text);
321             }
322             } else {
323 0         0 $self->set_status_error(400=>'Error: JSON input "options" not an object.');
324             }
325             } else {
326 0         0 $self->set_status_error(400=>'Error: "driver" not defined in JSON payload, environment variable SMS_SEND_ADAPTER_NODE_RED_DRIVER, or in SMS-Send.ini.');
327             }
328             }
329 2         14 return $self->{'SMS'};
330             }
331              
332             =head1 SEE ALSO
333              
334             L, L, L
335              
336             =head1 AUTHOR
337              
338             Michael R. Davis
339              
340             =head1 COPYRIGHT AND LICENSE
341              
342             MIT License
343              
344             Copyright (c) 2020 Michael R. Davis
345              
346             Permission is hereby granted, free of charge, to any person obtaining a copy
347             of this software and associated documentation files (the "Software"), to deal
348             in the Software without restriction, including without limitation the rights
349             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
350             copies of the Software, and to permit persons to whom the Software is
351             furnished to do so, subject to the following conditions:
352              
353             The above copyright notice and this permission notice shall be included in all
354             copies or substantial portions of the Software.
355              
356             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
357             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
358             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
359             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
360             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
361             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
362             SOFTWARE.
363              
364             =cut
365              
366             1;