From 7a79ad2fec72528894510e3fc9e559f35327766a Mon Sep 17 00:00:00 2001 From: Gerhard Jungwirth Date: Tue, 27 May 2014 10:55:33 +0200 Subject: [PATCH] MT#7211 API autoattendants --- .../Panel/Controller/API/AutoAttendants.pm | 137 +++++++++++++ .../Controller/API/AutoAttendantsItem.pm | 180 ++++++++++++++++++ .../Panel/Form/Subscriber/AutoAttendantAPI.pm | 72 +++++++ lib/NGCP/Panel/Role/API/AutoAttendants.pm | 147 ++++++++++++++ 4 files changed, 536 insertions(+) create mode 100644 lib/NGCP/Panel/Controller/API/AutoAttendants.pm create mode 100644 lib/NGCP/Panel/Controller/API/AutoAttendantsItem.pm create mode 100644 lib/NGCP/Panel/Form/Subscriber/AutoAttendantAPI.pm create mode 100644 lib/NGCP/Panel/Role/API/AutoAttendants.pm diff --git a/lib/NGCP/Panel/Controller/API/AutoAttendants.pm b/lib/NGCP/Panel/Controller/API/AutoAttendants.pm new file mode 100644 index 0000000000..b4aab54a9f --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/AutoAttendants.pm @@ -0,0 +1,137 @@ +package NGCP::Panel::Controller::API::AutoAttendants; +use Sipwise::Base; +use namespace::sweep; +use boolean qw(true); +use Data::HAL qw(); +use Data::HAL::Link qw(); +use HTTP::Headers qw(); +use HTTP::Status qw(:constants); +use MooseX::ClassAttribute qw(class_has); +use NGCP::Panel::Utils::DateTime; +use NGCP::Panel::Utils::Subscriber; +use NGCP::Panel::Utils::Preferences; +use Path::Tiny qw(path); +use Safe::Isa qw($_isa); +use UUID; +BEGIN { extends 'Catalyst::Controller::ActionRole'; } +require Catalyst::ActionRole::ACL; +require Catalyst::ActionRole::CheckTrailingSlash; +require Catalyst::ActionRole::HTTPMethods; +require Catalyst::ActionRole::RequireSSL; + +class_has 'api_description' => ( + is => 'ro', + isa => 'Str', + default => + 'Show a collection of auto attendant slots, belonging to a specific subscriber.', +); + +class_has 'query_params' => ( + is => 'ro', + isa => 'ArrayRef', + default => sub {[ + ]}, +); + +with 'NGCP::Panel::Role::API::AutoAttendants'; + +class_has('resource_name', is => 'ro', default => 'autoattendants'); +class_has('dispatch_path', is => 'ro', default => '/api/autoattendants/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-autoattendants'); + +__PACKAGE__->config( + action => { + map { $_ => { + ACLDetachTo => '/api/root/invalid_user', + AllowedRole => [qw/admin reseller/], + Args => 0, + Does => [qw(ACL CheckTrailingSlash RequireSSL)], + Method => $_, + Path => __PACKAGE__->dispatch_path, + } } @{ __PACKAGE__->allowed_methods }, + }, + action_roles => [qw(HTTPMethods)], +); + +sub auto :Private { + my ($self, $c) = @_; + + $self->set_body($c); + $self->log_request($c); + return 1; +} + +sub GET :Allow { + my ($self, $c) = @_; + my $page = $c->request->params->{page} // 1; + my $rows = $c->request->params->{rows} // 10; + { + my $subscribers = $self->item_rs($c); + (my $total_count, $subscribers) = $self->paginate_order_collection($c, $subscribers); + my (@embedded, @links); + for my $subscriber ($subscribers->search({}, {order_by => {-asc => 'me.id'}})->all) { + push @embedded, $self->hal_from_item($c, $subscriber); + push @links, Data::HAL::Link->new( + relation => 'ngcp:'.$self->resource_name, + href => sprintf('%s%d', $self->dispatch_path, $subscriber->id), + ); + } + push @links, + Data::HAL::Link->new( + relation => 'curies', + href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}', + name => 'ngcp', + templated => true, + ), + Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), + Data::HAL::Link->new(relation => 'self', href => sprintf('%s?page=%s&rows=%s', $self->dispatch_path, $page, $rows)); + if(($total_count / $rows) > $page ) { + push @links, Data::HAL::Link->new(relation => 'next', href => sprintf('%s?page=%d&rows=%d', $self->dispatch_path, $page + 1, $rows)); + } + if($page > 1) { + push @links, Data::HAL::Link->new(relation => 'prev', href => sprintf('/%s?page=%d&rows=%d', $c->request->path, $page - 1, $rows)); + } + + my $hal = Data::HAL->new( + embedded => [@embedded], + links => [@links], + ); + $hal->resource({ + total_count => $total_count, + }); + my $response = HTTP::Response->new(HTTP_OK, undef, + HTTP::Headers->new($hal->http_headers(skip_links => 1)), $hal->as_json); + $c->response->headers($response->headers); + $c->response->body($response->content); + return; + } + return; +} + +sub HEAD :Allow { + my ($self, $c) = @_; + $c->forward(qw(GET)); + $c->response->body(q()); + return; +} + +sub OPTIONS :Allow { + my ($self, $c) = @_; + my $allowed_methods = $self->allowed_methods; + $c->response->headers(HTTP::Headers->new( + Allow => $allowed_methods->join(', '), + Accept_Post => 'application/hal+json; profile=http://purl.org/sipwise/ngcp-api/#rel-'.$self->resource_name, + )); + $c->response->content_type('application/json'); + $c->response->body(JSON::to_json({ methods => $allowed_methods })."\n"); + return; +} + +sub end : Private { + my ($self, $c) = @_; + + $self->log_response($c); + return 1; +} + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/AutoAttendantsItem.pm b/lib/NGCP/Panel/Controller/API/AutoAttendantsItem.pm new file mode 100644 index 0000000000..50c2bc9f16 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/AutoAttendantsItem.pm @@ -0,0 +1,180 @@ +package NGCP::Panel::Controller::API::AutoAttendantsItem; +use Sipwise::Base; +use namespace::sweep; +use boolean qw(true); +use Data::HAL qw(); +use Data::HAL::Link qw(); +use HTTP::Headers qw(); +use HTTP::Status qw(:constants); +use MooseX::ClassAttribute qw(class_has); +use NGCP::Panel::Utils::ValidateJSON qw(); +use NGCP::Panel::Utils::DateTime; +use Path::Tiny qw(path); +use Safe::Isa qw($_isa); +BEGIN { extends 'Catalyst::Controller::ActionRole'; } +require Catalyst::ActionRole::ACL; +require Catalyst::ActionRole::HTTPMethods; +require Catalyst::ActionRole::RequireSSL; + +with 'NGCP::Panel::Role::API::AutoAttendants'; + +class_has('resource_name', is => 'ro', default => 'autoattendants'); +class_has('dispatch_path', is => 'ro', default => '/api/autoattendants/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-autoattendants'); + +__PACKAGE__->config( + action => { + map { $_ => { + ACLDetachTo => '/api/root/invalid_user', + AllowedRole => [qw/admin reseller/], + Args => 1, + Does => [qw(ACL RequireSSL)], + Method => $_, + Path => __PACKAGE__->dispatch_path, + } } @{ __PACKAGE__->allowed_methods }, + }, + action_roles => [qw(HTTPMethods)], +); + +sub auto :Private { + my ($self, $c) = @_; + + $self->set_body($c); + $self->log_request($c); + return 1; +} + +sub GET :Allow { + my ($self, $c, $id) = @_; + { + last unless $self->valid_id($c, $id); + my $item = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, subscriber => $item); + + my $hal = $self->hal_from_item($c, $item); + + my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( + (map { # XXX Data::HAL must be able to generate links with multiple relations + s|rel="(http://purl.org/sipwise/ngcp-api/#rel-resellers)"|rel="item $1"|r + =~ s/rel=self/rel="item self"/r; + } $hal->http_headers), + ), $hal->as_json); + $c->response->headers($response->headers); + $c->response->body($response->content); + return; + } + return; +} + +sub HEAD :Allow { + my ($self, $c, $id) = @_; + $c->forward(qw(GET)); + $c->response->body(q()); + return; +} + +sub OPTIONS :Allow { + my ($self, $c, $id) = @_; + my $allowed_methods = $self->allowed_methods; + $c->response->headers(HTTP::Headers->new( + Allow => $allowed_methods->join(', '), + Accept_Patch => 'application/json-patch+json', + )); + $c->response->content_type('application/json'); + $c->response->body(JSON::to_json({ methods => $allowed_methods })."\n"); + return; +} + +sub PUT :Allow { + my ($self, $c, $id) = @_; + my $schema = $c->model('DB'); + my $guard = $schema->txn_scope_guard; + { + my $preference = $self->require_preference($c); + last unless $preference; + + my $subscriber = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, subscriber => $subscriber); + my $resource = $self->get_valid_put_data( + c => $c, + id => $id, + media_type => 'application/json', + ); + last unless $resource; + + my $form = $self->get_form($c); + $subscriber = $self->update_item($c, $subscriber, undef, $resource, $form); + last unless $subscriber; + + $guard->commit; + + if ('minimal' eq $preference) { + $c->response->status(HTTP_NO_CONTENT); + $c->response->header(Preference_Applied => 'return=minimal'); + $c->response->body(q()); + } else { + my $hal = $self->hal_from_item($c, $subscriber); + my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( + $hal->http_headers, + ), $hal->as_json); + $c->response->headers($response->headers); + $c->response->header(Preference_Applied => 'return=representation'); + $c->response->body($response->content); + } + } + return; +} + +sub PATCH :Allow { + my ($self, $c, $id) = @_; + my $schema = $c->model('DB'); + my $guard = $schema->txn_scope_guard; + { + my $preference = $self->require_preference($c); + last unless $preference; + + my $subscriber = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, subscriber => $subscriber); + my $json = $self->get_valid_patch_data( + c => $c, + id => $id, + media_type => 'application/json-patch+json', + ops => ["add", "replace", "copy", "remove"], + ); + last unless $json; + + my $form = $self->get_form($c); + my $old_resource = $self->hal_from_item($c, $subscriber)->resource; + my $resource = $self->apply_patch($c, $old_resource, $json); + last unless $resource; + + $subscriber = $self->update_item($c, $subscriber, undef, $resource, $form); + last unless $subscriber; + + $guard->commit; + + if ('minimal' eq $preference) { + $c->response->status(HTTP_NO_CONTENT); + $c->response->header(Preference_Applied => 'return=minimal'); + $c->response->body(q()); + } else { + my $hal = $self->hal_from_item($c, $subscriber); + my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( + $hal->http_headers, + ), $hal->as_json); + $c->response->headers($response->headers); + $c->response->header(Preference_Applied => 'return=representation'); + $c->response->body($response->content); + } + } + return; +} + +sub end : Private { + my ($self, $c) = @_; + + $self->log_response($c); + return 1; +} + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Form/Subscriber/AutoAttendantAPI.pm b/lib/NGCP/Panel/Form/Subscriber/AutoAttendantAPI.pm new file mode 100644 index 0000000000..a2367b52b1 --- /dev/null +++ b/lib/NGCP/Panel/Form/Subscriber/AutoAttendantAPI.pm @@ -0,0 +1,72 @@ +package NGCP::Panel::Form::Subscriber::AutoAttendantAPI; + +use HTML::FormHandler::Moose; +extends 'HTML::FormHandler'; +use Moose::Util::TypeConstraints; + +has '+widget_wrapper' => ( default => 'Bootstrap' ); +sub build_render_list {return [qw/submitid fields actions/]} +sub build_form_element_class {return [qw(form-horizontal)]} + +has_field 'slots' => ( + type => 'Repeatable', + label => 'IVR Slots', + required => 1, +); + +has_field 'slots.slot' => ( + type => 'Select', + label => 'Key', + required => 1, + options => [ + { label => '0', value => 0 }, + { label => '1', value => 1 }, + { label => '2', value => 2 }, + { label => '3', value => 3 }, + { label => '4', value => 4 }, + { label => '5', value => 5 }, + { label => '6', value => 6 }, + { label => '7', value => 7 }, + { label => '8', value => 8 }, + { label => '9', value => 9 }, + ], + element_attr => { + rel => ['tooltip'], + title => ['The IVR key to press for this destination'], + }, +); + +has_field 'slots.destination' => ( + type => 'Text', + label => 'Destination', + required => 1, + element_attr => { + rel => ['tooltip'], + title => ['The destination for this slot; can be a number, username or full SIP URI.'], + }, +); + +has_block 'fields' => ( + tag => 'div', + class => [qw/modal-body/], + render_list => [qw/slots/], +); + +has_field 'subscriber_id' => ( + type => '+NGCP::Panel::Field::PosInteger', +); + +sub validate_slots_destination { + my ($self, $field) = @_; + + $field->clear_errors; + # TODO: proper SIP URI check + unless($field->value =~ /^(sip:)?[^\@]+(\@.+)?$/) { + my $err_msg = 'Invalid destination, must be number, username or SIP URI'; + $field->add_error($err_msg); + } + return; +} + +1; +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Role/API/AutoAttendants.pm b/lib/NGCP/Panel/Role/API/AutoAttendants.pm new file mode 100644 index 0000000000..7a851f7f09 --- /dev/null +++ b/lib/NGCP/Panel/Role/API/AutoAttendants.pm @@ -0,0 +1,147 @@ +package NGCP::Panel::Role::API::AutoAttendants; +use Moose::Role; +with 'NGCP::Panel::Role::API' => { + -alias =>{ item_rs => '_item_rs', }, + -excludes => [ 'item_rs' ], +}; + +use boolean qw(true); +use TryCatch; +use Data::HAL qw(); +use Data::HAL::Link qw(); +use HTTP::Status qw(:constants); +use JSON::Types; +use Test::More; +use NGCP::Panel::Form::Subscriber::AutoAttendantAPI; +use NGCP::Panel::Utils::Subscriber; + +sub get_form { + my ($self, $c) = @_; + + return NGCP::Panel::Form::Subscriber::AutoAttendantAPI->new; +} + +sub hal_from_item { + my ($self, $c, $item) = @_; + + my $p_subs = $item->provisioning_voip_subscriber; + my $resource = { subscriber_id => $item->id, slots => $self->autoattendants_from_subscriber($p_subs) }; + + my $hal = Data::HAL->new( + links => [ + Data::HAL::Link->new( + relation => 'curies', + href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}', + name => 'ngcp', + templated => true, + ), + Data::HAL::Link->new(relation => 'collection', href => sprintf("/api/%s/", $self->resource_name)), + Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), + Data::HAL::Link->new(relation => 'self', href => sprintf("%s%d", $self->dispatch_path, $item->id)), + Data::HAL::Link->new(relation => 'ngcp:autoattendants', href => sprintf("/api/autoattendants/%d", $item->id)), + Data::HAL::Link->new(relation => 'ngcp:subscribers', href => sprintf("/api/subscribers/%d", $item->id)), + ], + relation => 'ngcp:'.$self->resource_name, + ); + + my $form = $self->get_form($c); + return unless $self->validate_form( + c => $c, + form => $form, + resource => $resource, + run => 0, + exceptions => ['subscriber_id'], + ); + + $hal->resource($resource); + return $hal; +} + +sub item_rs { + my ($self, $c) = @_; + + my $item_rs; + $item_rs = $c->model('DB')->resultset('voip_subscribers') + ->search({ status => { '!=' => 'terminated' } }, + {join => 'provisioning_voip_subscriber'}); + if($c->user->roles eq "admin") { + } elsif($c->user->roles eq "reseller") { + $item_rs = $item_rs->search({ + 'contact.reseller_id' => $c->user->reseller_id, + }, { + join => { 'contract' => 'contact' }, + }); + } + + return $item_rs; +} + +sub item_by_id { + my ($self, $c, $id) = @_; + + my $item_rs = $self->item_rs($c); + return $item_rs->find($id); +} + +sub update_item { + my ($self, $c, $item, $old_resource, $resource, $form) = @_; + # $old_resource is unused + + my $billing_subs = $item; + my $prov_subs = $billing_subs->provisioning_voip_subscriber; + my $aa_rs = $prov_subs->voip_pbx_autoattendants; + + if (ref $resource->{slots} ne "ARRAY") { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid field 'slots'. Must be an array."); + return; + } + + $form //= $self->get_form($c); + return unless $self->validate_form( + c => $c, + form => $form, + resource => $resource, + ); + + try { + my $domain = $prov_subs->domain->domain // ''; + $aa_rs->delete; + for my $aa (@{ $resource->{slots} }) { + $aa_rs->create({ + destination => $self->get_sip_uri($aa->{destination}, $domain), + choice => $aa->{slot}, + }); + } + } catch($e) { + $c->log->error("failed to update speeddials: $e"); + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to update speeddials."); + return; + }; + + return $billing_subs; +} + +sub autoattendants_from_subscriber { + my ($self, $prov_subscriber) = @_; + + my @aas; + for my $s ($prov_subscriber->voip_pbx_autoattendants->all) { + push @aas, {destination => $s->destination, slot => $s->choice}; + } + return \@aas; +} + +sub get_sip_uri { + my ($self, $d, $domain) = @_; + + if($d !~ /\@/) { + $d .= '@'.$domain; + } + if($d !~ /^sip:/) { + $d = 'sip:' . $d; + } + return $d; +} + +1; +# vim: set tabstop=4 expandtab: