diff --git a/lib/NGCP/Panel/Controller/API/CallLists.pm b/lib/NGCP/Panel/Controller/API/CallLists.pm new file mode 100644 index 0000000000..74560a7812 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/CallLists.pm @@ -0,0 +1,244 @@ +package NGCP::Panel::Controller::API::CallLists; +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 Path::Tiny qw(path); +use Safe::Isa qw($_isa); +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 => + 'Defines call lists in simplified form for showing call histories of subscribers.', +); + +class_has 'query_params' => ( + is => 'ro', + isa => 'ArrayRef', + default => sub {[ + { + param => 'subscriber_id', + description => 'Filter for calls for a specific subscriber. Mandatory if called by admin, reseller or subscriberadmin to filter list down to a specific subscriber in order to properly determine the direction of calls.', + query => { + first => sub { + my $q = shift; + return { + -or => [ + 'source_subscriber.id' => $q, + 'destination_subscriber.id' => $q, + ], + }; + }, + second => sub { + return { + join => ['source_subscriber', 'destination_subscriber'], + }; + }, + }, + }, + { + param => 'status', + description => 'Filter for calls with a specific status. One of "ok", "busy", "noanswer", "cancel", "offline", "timeout", "other".', + query => { + first => sub { + my $q = shift; + { + call_status => $q, + }; + }, + second => sub {}, + }, + }, + { + param => 'status_ne', + description => 'Filter for calls not having a specific status. One of "ok", "busy", "noanswer", "cancel", "offline", "timeout", "other".', + query => { + first => sub { + my $q = shift; + { + call_status => { '!=' => $q }, + }; + }, + second => sub {}, + }, + }, + { + param => 'direction', + description => 'Filter for calls with a specific direction. One of "in", "out".', + query => { + first => sub { + my ($q, $c) = @_; + if($q eq "out") { + { + source_user_id => $c->user->uuid, + }; + } elsif($q eq "in") { + { + destination_user_id => $c->user->uuid, + }; + } + }, + second => sub {}, + }, + }, + { + param => 'start_ge', + description => 'Filter for calls starting greater or equal the specified time stamp.', + query => { + first => sub { + my $q = shift; + my $dt = NGCP::Panel::Utils::DateTime::from_string($q); + { start_time => { '>=' => $dt->epoch } }, + }, + second => sub {}, + }, + }, + { + param => 'start_le', + description => 'Filter for calls starting lower or equal the specified time stamp.', + query => { + first => sub { + my $q = shift; + $q .= ' 23:59:59' if($q =~ /^\d{4}\-\d{2}\-\d{2}$/); + my $dt = NGCP::Panel::Utils::DateTime::from_string($q); + { start_time => { '<=' => $dt->epoch } }, + }, + second => sub {}, + }, + }, + ]}, +); + +with 'NGCP::Panel::Role::API::CallLists'; + +class_has('resource_name', is => 'ro', default => 'calllists'); +class_has('dispatch_path', is => 'ro', default => '/api/calllists/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-calllists'); + +__PACKAGE__->config( + action => { + map { $_ => { + ACLDetachTo => '/api/root/invalid_user', + AllowedRole => [qw/admin reseller subscriberadmin subscriber/], + 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); +} + +sub GET :Allow { + my ($self, $c) = @_; + my $page = $c->request->params->{page} // 1; + my $rows = $c->request->params->{rows} // 10; + my $schema = $c->model('DB'); + { + my $sub; + if($c->user->roles ne "subscriber") { + unless($c->req->param('subscriber_id')) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Mandatory parameter 'subscriber_id' missing in request"); + last; + } + $sub = $schema->resultset('voip_subscribers')->find($c->req->param('subscriber_id')); + unless($sub) { + $self->error($c, HTTP_NOT_FOUND, "Invalid 'subscriber_id'."); + last; + } + if(($c->user->roles eq "subscriberadmin" && $sub->contract_id != $c->user->account_id) || + ($c->user->roles eq "reseller" && $sub->contract->contact->reseller_id != $c->user->reseller_id)) { + $self->error($c, HTTP_NOT_FOUND, "Invalid 'subscriber_id'."); + last; + } + } else { + $sub = $c->user->voip_subscriber; + } + + my $items = $self->item_rs($c); + (my $total_count, $items) = $self->paginate_order_collection($c, $items); + my (@embedded, @links); + my $form = $self->get_form($c); + for my $item ($items->all) { + push @embedded, $self->hal_from_item($c, $item, $sub, $form); + push @links, Data::HAL::Link->new( + relation => 'ngcp:'.$self->resource_name, + href => sprintf('/%s%d', $c->request->path, $item->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', $c->request->path, $page, $rows)); + if(($total_count / $rows) > $page ) { + push @links, Data::HAL::Link->new(relation => 'next', href => sprintf('/%s?page=%d&rows=%d', $c->request->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_filtered($c); + $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); +} + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Form/CallList/Subscriber.pm b/lib/NGCP/Panel/Form/CallList/Subscriber.pm new file mode 100644 index 0000000000..8886061ee3 --- /dev/null +++ b/lib/NGCP/Panel/Form/CallList/Subscriber.pm @@ -0,0 +1,106 @@ +package NGCP::Panel::Form::CallList::Subscriber; + +use HTML::FormHandler::Moose; +extends 'HTML::FormHandler'; +use Moose::Util::TypeConstraints; + +use HTML::FormHandler::Widget::Block::Bootstrap; + +has '+widget_wrapper' => ( default => 'Bootstrap' ); +has_field 'submitid' => ( type => 'Hidden' ); + +has_field 'direction' => ( + type => 'Select', + required => 1, + options => [ + { label => "in", value => "in" }, + { label => "out", value => "out" }, + ], + element_attr => { + rel => ['tooltip'], + title => ['Call direction, either "in" or "out"'] + }, +); + +has_field 'other_cli' => ( + type => 'Text', + required => 1, + element_attr => { + rel => ['tooltip'], + title => ['The CLI of the other party.'] + }, +); + +has_field 'status' => ( + type => 'Select', + required => 1, + options => [ + { label => 'ok', value => 'ok' }, + { label => 'busy', value => 'busy' }, + { label => 'noanswer', value => 'noanswer' }, + { label => 'cancel', value => 'cancel' }, + { label => 'offline', value => 'offline' }, + { label => 'timeout', value => 'timeout' }, + { label => 'other', value => 'other' }, + ], + element_attr => { + rel => ['tooltip'], + title => ['The status of the call, one of ok, busy, noanswer, cancel, offline, timeout, other.'] + }, +); + +has_field 'type' => ( + type => 'Select', + required => 1, + options => [ + { label => 'call', value => 'call' }, + { label => 'cfu', value => 'cfu' }, + { label => 'cfb', value => 'cfb' }, + { label => 'cft', value => 'cft' }, + { label => 'cfna', value => 'cfna' }, + ], + element_attr => { + rel => ['tooltip'], + title => ['The type of call, one of call, cfu, cfb, cft, cfna.'] + }, +); + +has_field 'start_time' => ( + type => 'Text', + required => 1, + element_attr => { + rel => ['tooltip'], + title => ['The timestamp of the call connection.'] + }, +); + +has_field 'duration' => ( + type => 'PosInteger', + required => 1, + element_attr => { + rel => ['tooltip'], + title => ['The duration of the call.'] + }, +); + +has_field 'customer_cost' => ( + type => 'Float', + required => 0, + element_attr => { + rel => ['tooltip'], + title => ['The cost for the customer.'] + }, +); + +has_field 'customer_free_time' => ( + type => 'PosInteger', + required => 0, + element_attr => { + rel => ['tooltip'], + title => ['The number of free seconds of the customer used for this call.'] + }, +); + +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Role/API/CallLists.pm b/lib/NGCP/Panel/Role/API/CallLists.pm new file mode 100644 index 0000000000..1f34399d9d --- /dev/null +++ b/lib/NGCP/Panel/Role/API/CallLists.pm @@ -0,0 +1,140 @@ +package NGCP::Panel::Role::API::CallLists; +use Moose::Role; +use Sipwise::Base; +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 POSIX; +use DateTime::Format::Strptime; +use NGCP::Panel::Utils::DateTime; +use NGCP::Panel::Utils::Subscriber; +use NGCP::Panel::Form::CallList::Subscriber; + +sub item_rs { + my ($self, $c) = @_; + + my $item_rs = $c->model('DB')->resultset('cdr'); + if($c->user->roles eq "admin") { + } elsif($c->user->roles eq "reseller") { + $item_rs = $item_rs->search({ + -or => [ + { source_provider_id => $c->user->reseller->contract_id }, + { destination_provider_id => $c->user->reseller->contract_id }, + ], + }); + } elsif($c->user->roles eq "subscriberadmin") { + $item_rs = $item_rs->search({ + -or => [ + { 'source_account_id' => $c->user->account_id }, + { 'destination_account_id' => $c->user->account_id }, + ], + }); + } else { + $item_rs = $item_rs->search({ + -or => [ + { 'source_subscriber.id' => $c->user->voip_subscriber->id }, + { 'destination_subscriber.id' => $c->user->voip_subscriber->id }, + ], + },{ + join => ['source_subscriber', 'destination_subscriber'], + }); + } + return $item_rs; +} + +sub get_form { + my ($self, $c) = @_; + if($c->user->roles eq "admin") { + return NGCP::Panel::Form::CallList::Subscriber->new; + } elsif($c->user->roles eq "reseller") { + return NGCP::Panel::Form::CallList::Subscriber->new; + } else { + return NGCP::Panel::Form::CallList::Subscriber->new; + } +} + +sub hal_from_item { + my ($self, $c, $item, $sub, $form) = @_; + my $resource = $self->resource_from_item($c, $item, $sub, $form); + + 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)), + # todo: customer can be in source_account_id or destination_account_id +# Data::HAL::Link->new(relation => 'ngcp:customers', href => sprintf("/api/customers/%d", $item->source_customer_id)), + ], + relation => 'ngcp:'.$self->resource_name, + ); + + $form //= $self->get_form($c); + + $self->validate_form( + c => $c, + resource => $resource, + form => $form, + run => 0, + exceptions => [], + ); + + $resource->{id} = int($item->id); + $hal->resource($resource); + return $hal; +} + +sub resource_from_item { + my ($self, $c, $item, $sub, $form) = @_; + my $resource = {}; + my $datetime_fmt = DateTime::Format::Strptime->new( + pattern => '%F %T', + ); + + $resource->{direction} = $sub->uuid eq $item->source_user_id ? + "out" : "in"; + $resource->{other_cli} = $resource->{direction} eq "out" ? + $item->destination_user_in : $item->source_cli; + if($resource->{direction} eq "in" && $item->source_clir) { + $resource->{other_cli} = undef; + } elsif($resource->{other_cli} !~ /^\d+$/) { + $resource->{other_cli} .= '@'.$item->destination_domain_in; + } else { + $resource->{other_cli} = NGCP::Panel::Utils::Subscriber::apply_rewrite( + c => $c, subscriber => $sub, + number => $resource->{other_cli}, direction => "caller_out" + ); + } + $resource->{status} = $item->call_status; + $resource->{type} = $item->call_type; + + $resource->{start_time} = $datetime_fmt->format_datetime($item->start_time); + $resource->{duration} = NGCP::Panel::Utils::DateTime::sec_to_hms(ceil($item->duration)); + $resource->{customer_cost} = $resource->{direction} eq "out" ? + $item->source_customer_cost : $item->destination_customer_cost; + $resource->{customer_free_time} = $resource->{direction} eq "out" ? + $item->source_customer_free_time : 0; + + return $resource; +} + +sub item_by_id { + my ($self, $c, $id) = @_; + my $item_rs = $self->item_rs($c); + return $item_rs->find($id); +} + +1; +# vim: set tabstop=4 expandtab: