RT# 83450 - fixed rateplan export
[freeside.git] / FS / FS / part_export / netsapiens.pm
1 package FS::part_export::netsapiens;
2 use base qw( FS::part_export );
3
4 use vars qw( $me %info );
5 use MIME::Base64;
6 use Tie::IxHash;
7 use Date::Format qw( time2str );
8 use Regexp::Common qw( URI );
9 use REST::Client;
10 use Carp qw(carp);
11
12 $me = '[FS::part_export::netsapiens]';
13
14 #These export options set default values for the various commands
15 #to create/update objects.  Add more options as needed.
16
17 my %tristate = ( type => 'select', options => [ '', 'yes', 'no' ]);
18
19 tie my %subscriber_fields, 'Tie::IxHash',
20   'admin_vmail'     => { label=>'VMail Prov.', %tristate },
21   'dial_plan'       => { label=>'Dial Translation' },
22   'dial_policy'     => { label=>'Dial Permission' },
23   'call_limit'      => { label=>'Call Limit' },
24   'domain_dir'      => { label=>'Dir Lst', %tristate },
25 ;
26
27 tie my %registrar_fields, 'Tie::IxHash',
28   'authenticate_register' => { label=>'Authenticate Registration', %tristate },
29   'authentication_realm'  => { label=>'Authentication Realm' },
30 ;
31
32 tie my %dialplan_fields, 'Tie::IxHash',
33   'responder'       => { label=>'Application' }, #this could be nicer
34   'from_name'       => { label=>'Source Name Translation' },
35   'from_user'       => { label=>'Source User Translation' },
36 ;
37
38 my %features = (
39   'for' => 'Forward',
40   'fnr' => 'Forward Not Registered',
41   'fna' => 'Forward No Answer',
42   'fbu' => 'Forward Busy',
43   'dnd' => 'Do-Not-Disturb',
44   'sim' => 'Simultaneous Ring',
45 );
46
47 my %feature_param = (
48   'dnd' => 'n/a',
49   'sim' => '$phonenum',
50 );
51
52 tie my %options, 'Tie::IxHash',
53   'login'           => { label=>'NetSapiens tac2 User API username' },
54   'password'        => { label=>'NetSapiens tac2 User API password' },
55   'url'             => { label=>'NetSapiens tac2 User URL' },
56   'device_login'    => { label=>'NetSapiens tac2 Device API username' },
57   'device_password' => { label=>'NetSapiens tac2 Device API password' },
58   'device_url'      => { label=>'NetSapiens tac2 Device URL' },
59   'domain'          => { label=>'NetSapiens Domain' },
60   'domain_no_tld'   => { label=>'Omit TLD from domains', type=>'checkbox' },
61   'debug'           => { label=>'Enable debugging', type=>'checkbox' },
62   %subscriber_fields,
63   'features'        => { label        => 'Default features',
64                          type         => 'select',
65                          multiple     => 1,
66                          options      => [ keys %features ],
67                          option_label => sub { $features{$_[0]}; },
68                        },
69   %registrar_fields,
70   %dialplan_fields,
71   'did_countrycode' => { label=>'Use country code in DID destination',
72                          type =>'checkbox' },
73 ;
74
75 %info = (
76   'svc'        => [qw( svc_phone part_device )],
77   'desc'       => 'Provision phone numbers to NetSapiens',
78   'options'    => \%options,
79   'no_machine' => 1,
80   'notes'      => <<'END'
81 END
82 );
83
84 # http://devguide.netsapiens.com/
85
86 sub rebless { shift; }
87
88
89 sub check_options {
90   my ($self, $options) = @_;
91         
92   my $rex = qr/$RE{URI}{HTTP}{-scheme => qr|https?|}/;                  # match any "http:" or "https:" URL
93         
94   for my $key (qw/url device_url/) {
95     if ($$options{$key} && ($$options{$key} !~ $rex)) {
96       return "Invalid (URL): " . $$options{$key};
97     }
98   }
99   return '';
100 }
101
102
103
104 sub ns_command {
105   my $self = shift;
106   $self->_ns_command('', @_);
107 }
108
109 sub ns_device_command { 
110   my $self = shift;
111   $self->_ns_command('device_', @_);
112 }
113
114 sub _ns_command {
115   my( $self, $prefix, $method, $command ) = splice(@_,0,4);
116
117   # kludge to curb excessive paranoia in LWP 6.0+
118   local $ENV{'PERL_LWP_SSL_VERIFY_HOSTNAME'} = 0;
119
120   my $ns = new REST::Client 'host'=>$self->option($prefix.'url');
121
122   my @args = ( $command );
123
124   if ( $method eq 'PUT' ) {
125     my $content = $ns->buildQuery( { @_ } );
126     $content =~ s/^\?//;
127     push @args, $content;
128   } elsif ( $method eq 'GET' ) {
129     $args[0] .= $ns->buildQuery( { @_ } );
130   }
131
132   warn "$me $method ". $self->option($prefix.'url'). join(', ', @args). "\n"
133     if $self->option('debug');
134
135   my $auth = encode_base64( $self->option($prefix.'login'). ':'.
136                             $self->option($prefix.'password')    );
137   push @args, { 'Authorization' => "Basic $auth" };
138
139   $ns->$method( @args );
140   $ns;
141 }
142
143 sub ns_domain {
144   my($self, $svc_phone) = (shift, shift);
145   my $domain = $svc_phone->domain || $self->option('domain');
146
147   $domain =~ s/\.\w{2,4}$//
148     if $self->option('domain_no_tld');
149   
150   $domain;
151 }
152
153 sub ns_subscriber {
154   my($self, $svc_phone) = (shift, shift);
155
156   my $domain = $self->ns_domain($svc_phone);
157   my $phonenum = $svc_phone->phonenum;
158
159   "/domains_config/$domain/subscriber_config/$phonenum";
160 }
161
162 sub ns_registrar {
163   my($self, $svc_phone) = (shift, shift);
164
165   $self->ns_subscriber($svc_phone).
166     '/registrar_config/'. $self->ns_devicename($svc_phone);
167 }
168
169 sub ns_feature {
170   my($self, $svc_phone, $feature) = (shift, shift, shift);
171
172   $self->ns_subscriber($svc_phone).
173     "/feature_config/$feature,*,*,*,*";
174
175 }
176
177 sub ns_devicename {
178   my( $self, $svc_phone ) = (shift, shift);
179
180   my $domain = $self->ns_domain($svc_phone);
181   #my $countrycode = $svc_phone->countrycode;
182   my $phonenum    = $svc_phone->phonenum;
183
184   #"sip:$countrycode$phonenum\@$domain";
185   "sip:$phonenum\@$domain";
186 }
187
188 sub ns_dialplan {
189   my($self, $svc_phone) = (shift, shift);
190
191   my $countrycode = $svc_phone->countrycode || '1';
192   my $phonenum    = $svc_phone->phonenum;
193   # Only in the dialplan destination, nowhere else
194   if ( $self->option('did_countrycode') ) {
195     $phonenum = $countrycode . $phonenum;
196   }
197
198   #"/dialplans/DID+Table/dialplan_config/sip:$countrycode$phonenum\@*"
199   "/domains_config/admin-only/dialplans/DID+Table/dialplan_config/sip:$phonenum\@*,*,*,*,*,*,*";
200 }
201
202 sub ns_device {
203   my($self, $svc_phone, $phone_device ) = (shift, shift, shift);
204
205   #my $countrycode = $svc_phone->countrycode;
206   #my $phonenum    = $svc_phone->phonenum;
207
208   "/phones_config/". lc($phone_device->mac_addr);
209 }
210
211 sub ns_create_or_update {
212   my($self, $svc_phone, $dial_policy) = (shift, shift, shift);
213
214   my $domain = $self->ns_domain($svc_phone);
215   #my $countrycode = $svc_phone->countrycode;
216   my $phonenum    = $svc_phone->phonenum;
217
218   #deal w/unaudited netsapiens services?
219   my $cust_main = $svc_phone->cust_svc->cust_pkg->cust_main;
220
221   my( $firstname, $lastname );
222   if ( $svc_phone->phone_name =~ /^\s*(\S+)\s+(\S.*\S)\s*$/ ) {
223     $firstname = $1;
224     $lastname  = $2;
225   } else {
226     $firstname = $cust_main->get('first');
227     $lastname  = $cust_main->get('last');
228   }
229
230   my ($email) = ($cust_main->invoicing_list_emailonly, '');
231   my $custnum = $cust_main->custnum;
232
233   ###
234   # Piece 1 (already done) - User creation
235   ###
236   
237   $phonenum =~ /^(\d{3})/;
238   my $area_code = $1;
239
240   my $ns = $self->ns_command( 'PUT', $self->ns_subscriber($svc_phone), 
241     'subscriber_login' => $phonenum.'@'.$domain,
242     'firstname'        => $firstname,
243     'lastname'         => $lastname,
244     'subscriber_pin'   => $svc_phone->pin,
245     'callid_name'      => "$firstname $lastname",
246     'callid_nmbr'      => $phonenum,
247     'callid_emgr'      => $phonenum,
248     'email_address'    => $email,
249     'area_code'        => $area_code,
250     'srv_code'         => $custnum,
251     'date_created'     => time2str('%Y-%m-%d %H:%M:%S', time),
252     $self->options_named(keys %subscriber_fields),
253     # allow this to be overridden for suspend
254     ( $dial_policy ? ('dial_policy' => $dial_policy) : () ),
255   );
256
257   if ( $ns->responseCode !~ /^2/ ) {
258      return $ns->responseCode. ' '.
259             join(', ', $self->ns_parse_response( $ns->responseContent ) );
260   }
261
262   ###
263   # Piece 1.5 - feature creation
264   ###
265   foreach $feature (split /\s+/, $self->option('features') ) {
266
267     my $param= exists($feature_param{$feature}) ? $feature_param{$feature} : '';
268     $param = $phonenum if $param eq '$phonenum';
269
270     my $nsf = $self->ns_command( 'PUT', $self->ns_feature($svc_phone, $feature),
271       'control'    => 'd', #User Control, disable
272       'expires'    => 'never',
273       #'ts'         => '', #?
274       'parameters' => $param,
275       'hour_match' => '*',
276       'time_frame' => '*',
277       'activation' => 'now',
278     );
279
280     if ( $nsf->responseCode !~ /^2/ ) {
281        return $nsf->responseCode. ' '.
282               join(', ', $self->ns_parse_response( $ns->responseContent ) );
283     }
284
285   }
286
287   ###
288   # Piece 2 - sip device creation 
289   ###
290
291   my $ns2 = $self->ns_command( 'PUT', $self->ns_registrar($svc_phone),
292     'termination_match' => $self->ns_devicename($svc_phone),
293     'authentication_key'=> $svc_phone->sip_password,
294     'srv_code'          => $custnum,
295     $self->options_named(keys %registrar_fields),
296   );
297
298   if ( $ns2->responseCode !~ /^2/ ) {
299      return $ns2->responseCode. ' '.
300             join(', ', $self->ns_parse_response( $ns2->responseContent ) );
301   }
302
303   ###
304   # Piece 3 - DID mapping to user
305   ###
306
307   my $ns3 = $self->ns_command( 'PUT', $self->ns_dialplan($svc_phone),
308     'to_user' => $phonenum,
309     'to_host' => $domain,
310     'plan_description' => "$custnum: $lastname, $firstname", #config?
311     $self->options_named(keys %dialplan_fields),
312   );
313
314   if ( $ns3->responseCode !~ /^2/ ) {
315      return $ns3->responseCode. ' '.
316             join(', ', $self->ns_parse_response( $ns3->responseContent ) );
317   }
318
319   '';
320 }
321
322 sub ns_delete {
323   my($self, $svc_phone) = (shift, shift);
324
325   # do the create steps in reverse order, though I'm not sure it matters
326
327   my $ns3 = $self->ns_command( 'DELETE', $self->ns_dialplan($svc_phone) );
328
329   if ( $ns3->responseCode !~ /^2/ ) {
330      return $ns3->responseCode. ' '.
331             join(', ', $self->ns_parse_response( $ns3->responseContent ) );
332   }
333
334   my $ns2 = $self->ns_command( 'DELETE', $self->ns_registrar($svc_phone) );
335
336   if ( $ns2->responseCode !~ /^2/ ) {
337      return $ns2->responseCode. ' '.
338             join(', ', $self->ns_parse_response( $ns2->responseContent ) );
339   }
340
341   my $ns = $self->ns_command( 'DELETE', $self->ns_subscriber($svc_phone) );
342
343   if ( $ns->responseCode !~ /^2/ ) {
344      return $ns->responseCode. ' '.
345             join(', ', $self->ns_parse_response( $ns->responseContent ) );
346   }
347
348   '';
349
350 }
351
352 sub ns_parse_response {
353   my( $self, $content ) = ( shift, shift );
354
355   #try to screen-scrape something useful
356   tie my %hash, Tie::IxHash;
357   while ( $content =~ s/^.*?<p>\s*<b>(.+?)<\/b>\s*(.+?)\s*<\/p>//is ) {
358     ( $hash{$1} = $2 ) =~ s/^\s*<(\w+)>(.+?)<\/\1>/$2/is;
359   }
360
361   %hash;
362 }
363
364 sub _export_insert {
365   my($self, $svc_phone) = (shift, shift);
366   $self->ns_create_or_update($svc_phone);
367 }
368
369 sub _export_replace {
370   my( $self, $new, $old ) = (shift, shift, shift);
371   return "can't change phonenum with NetSapiens (unprovision and reprovision?)"
372     if $old->phonenum ne $new->phonenum;
373   $self->_export_insert($new);
374 }
375
376 sub _export_delete {
377   my( $self, $svc_phone ) = (shift, shift);
378
379   $self->ns_delete($svc_phone);
380 }
381
382 sub _export_suspend {
383   my( $self, $svc_phone ) = (shift, shift);
384   $self->ns_create_or_update($svc_phone, 'Deny');
385 }
386
387 sub _export_unsuspend {
388   my( $self, $svc_phone ) = (shift, shift);
389   #$self->ns_create_or_update($svc_phone, 'Permit All');
390   $self->_export_insert($svc_phone);
391 }
392
393 sub export_device_insert {
394   my( $self, $svc_phone, $phone_device ) = (shift, shift, shift);
395
396   if ( $FS::svc_Common::noexport_hack ) {
397     carp 'export_device_insert() suppressed by noexport_hack'
398       if $self->option('debug');
399     return;
400   }
401
402   my $domain = $self->ns_domain($svc_phone);
403   my $countrycode = $svc_phone->countrycode;
404   my $phonenum    = $svc_phone->phonenum;
405
406   my $ns = $self->ns_device_command(
407     'PUT', $self->ns_device($svc_phone, $phone_device),
408       'line1_enable' => 'yes',
409       'device1'      => $self->ns_devicename($svc_phone),
410       'line1_ext'    => $phonenum,
411 ,
412       #'line2_enable' => 'yes',
413       #'device2'      =>
414       #'line2_ext'    =>
415
416       #'notes' => 
417       'server'       => 'SiPbx',
418       'domain'       => $domain,
419
420       'brand'        => $phone_device->part_device->devicename,
421       
422   );
423
424   if ( $ns->responseCode !~ /^2/ ) {
425      return $ns->responseCode. ' '.
426             join(', ', $self->ns_parse_response( $ns->responseContent ) );
427   }
428
429   '';
430
431 }
432
433 sub export_device_delete {
434   my( $self, $svc_phone, $phone_device ) = (shift, shift, shift);
435
436   if ( $FS::svc_Common::noexport_hack ) {
437     carp 'export_device_delete() suppressed by noexport_hack'
438       if $self->option('debug');
439     return;
440   }
441
442   my $ns = $self->ns_device_command(
443     'DELETE', $self->ns_device($svc_phone, $phone_device),
444   );
445
446   if ( $ns->responseCode !~ /^2/ ) {
447      return $ns->responseCode. ' '.
448             join(', ', $self->ns_parse_response( $ns->responseContent ) );
449   }
450
451   '';
452
453 }
454
455
456 sub export_device_replace {
457   my( $self, $svc_phone, $new_phone_device, $old_phone_device ) =
458     (shift, shift, shift, shift);
459
460   #?
461   $self->export_device_insert( $svc_phone, $new_phone_device );
462
463 }
464
465 sub export_links {
466   my($self, $svc_phone, $arrayref) = (shift, shift, shift);
467   #push @$arrayref, qq!<A HREF="http://example.com/~!. $svc_phone->username.
468   #                 qq!">!. $svc_phone->username. qq!</A>!;
469   '';
470 }
471
472 sub options_named {
473   my $self = shift;
474   map { 
475         my $v = $self->option($_);
476         length($v) ? ($_ => $v) : ()
477       } @_
478 }
479
480 1;