diff --git a/lib/NGCP/Panel/Controller/API/Root.pm b/lib/NGCP/Panel/Controller/API/Root.pm
index e11acbf073..0484920ed3 100644
--- a/lib/NGCP/Panel/Controller/API/Root.pm
+++ b/lib/NGCP/Panel/Controller/API/Root.pm
@@ -53,8 +53,9 @@ sub GET : Allow {
next if(exists $blacklist->{$mod});
my $rel = lc $mod;
my $full_mod = 'NGCP::Panel::Controller::API::'.$mod;
+ my $full_item_mod = 'NGCP::Panel::Controller::API::'.$mod.'Item';
- my $role = $full_mod->config->{action}->{GET}->{AllowedRole};
+ my $role = $full_mod->config->{action}->{OPTIONS}->{AllowedRole};
if(ref $role eq "ARRAY") {
next unless grep @{ $role }, $c->user->roles;
} else {
@@ -65,6 +66,9 @@ sub GET : Allow {
if($full_mod->can('query_params')) {
$query_params = $full_mod->query_params;
}
+ my $actions = [ keys %{ $full_mod->config->{action} } ];
+ my $item_actions = [ keys %{ $full_item_mod->config->{action} } ];
+
my $form = $full_mod->get_form($c);
$c->stash->{collections}->{$rel} = {
@@ -72,7 +76,10 @@ sub GET : Allow {
description => $full_mod->api_description,
fields => $form ? $self->get_collection_properties($form) : [],
query_params => $query_params,
+ actions => $actions,
+ item_actions => $item_actions,
};
+
}
$c->stash(template => 'api/root.tt');
diff --git a/lib/NGCP/Panel/Controller/API/VoicemailRecordings.pm b/lib/NGCP/Panel/Controller/API/VoicemailRecordings.pm
new file mode 100644
index 0000000000..28829f7e7a
--- /dev/null
+++ b/lib/NGCP/Panel/Controller/API/VoicemailRecordings.pm
@@ -0,0 +1,71 @@
+package NGCP::Panel::Controller::API::VoicemailRecordings;
+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 the actual recording of voicemail messages. It is referred to by the ngcp:voicemails relation. A GET on an item returns the binary blob of the recording with Content-Type "audio/x-wav".',
+);
+
+with 'NGCP::Panel::Role::API::VoicemailRecordings';
+
+class_has('resource_name', is => 'ro', default => 'voicemailrecordings');
+class_has('dispatch_path', is => 'ro', default => '/api/voicemailrecordings/');
+class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-voicemailrecordings');
+
+__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);
+}
+
+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);
+}
+
+# vim: set tabstop=4 expandtab:
diff --git a/lib/NGCP/Panel/Controller/API/VoicemailRecordingsItem.pm b/lib/NGCP/Panel/Controller/API/VoicemailRecordingsItem.pm
new file mode 100644
index 0000000000..b5ae962892
--- /dev/null
+++ b/lib/NGCP/Panel/Controller/API/VoicemailRecordingsItem.pm
@@ -0,0 +1,83 @@
+package NGCP::Panel::Controller::API::VoicemailRecordingsItem;
+use Sipwise::Base;
+use namespace::sweep;
+use HTTP::Headers qw();
+use HTTP::Status qw(:constants);
+use MooseX::ClassAttribute qw(class_has);
+use NGCP::Panel::Utils::DateTime;
+use NGCP::Panel::Utils::ValidateJSON qw();
+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::VoicemailRecordings';
+
+class_has('resource_name', is => 'ro', default => 'voicemailrecordings');
+class_has('dispatch_path', is => 'ro', default => '/api/voicemailrecordings/');
+class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-voicemailrecordings');
+
+__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);
+}
+
+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, voicemailrecording => $item);
+
+ $c->response->header ('Content-Disposition' => 'attachment; filename="' . $self->resource_name . '-' . $item->id . '.wav"');
+ $c->response->content_type('audio/x-wav');
+ $c->response->body($item->recording);
+ 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 end : Private {
+ my ($self, $c) = @_;
+
+ #$self->log_response($c);
+}
+
+# vim: set tabstop=4 expandtab:
diff --git a/lib/NGCP/Panel/Controller/API/Voicemails.pm b/lib/NGCP/Panel/Controller/API/Voicemails.pm
new file mode 100644
index 0000000000..c561e9893f
--- /dev/null
+++ b/lib/NGCP/Panel/Controller/API/Voicemails.pm
@@ -0,0 +1,147 @@
+package NGCP::Panel::Controller::API::Voicemails;
+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 the meta information like duration, callerid etc for voicemail recordings. The actual recordings can be fetched via the related ngcp:voicemailrecordings relation.',
+);
+
+class_has 'query_params' => (
+ is => 'ro',
+ isa => 'ArrayRef',
+ default => sub {[
+ {
+ param => 'subscriber_id',
+ description => 'Filter for voicemails belonging to a specific subscriber',
+ query => {
+ first => sub {
+ my $q = shift;
+ { 'voip_subscriber.id' => $q };
+ },
+ second => sub { join => 'foo' },
+ },
+ },
+ ]},
+);
+
+with 'NGCP::Panel::Role::API::Voicemails';
+
+class_has('resource_name', is => 'ro', default => 'voicemails');
+class_has('dispatch_path', is => 'ro', default => '/api/voicemails/');
+class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-voicemails');
+
+__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);
+}
+
+sub GET :Allow {
+ my ($self, $c) = @_;
+ my $page = $c->request->params->{page} // 1;
+ my $rows = $c->request->params->{rows} // 10;
+ {
+ my $items = $self->item_rs($c);
+ my $total_count = int($items->count);
+ $items = $items->search(undef, {
+ page => $page,
+ rows => $rows,
+ });
+ my (@embedded, @links);
+ for my $item ($items->search({}, {order_by => {-asc => 'me.id'}})->all) {
+ push @embedded, $self->hal_from_item($c, $item);
+ 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;
+ $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/Controller/API/VoicemailsItem.pm b/lib/NGCP/Panel/Controller/API/VoicemailsItem.pm
new file mode 100644
index 0000000000..aa3753de36
--- /dev/null
+++ b/lib/NGCP/Panel/Controller/API/VoicemailsItem.pm
@@ -0,0 +1,192 @@
+package NGCP::Panel::Controller::API::VoicemailsItem;
+use Sipwise::Base;
+use namespace::sweep;
+use HTTP::Headers qw();
+use HTTP::Status qw(:constants);
+use MooseX::ClassAttribute qw(class_has);
+use NGCP::Panel::Utils::DateTime;
+use NGCP::Panel::Utils::ValidateJSON qw();
+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::Voicemails';
+
+class_has('resource_name', is => 'ro', default => 'voicemails');
+class_has('dispatch_path', is => 'ro', default => '/api/voicemails/');
+class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-voicemails');
+
+__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);
+}
+
+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, voicemail => $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"|;
+ s/rel=self/rel="item self"/;
+ $_
+ } $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 PATCH :Allow {
+ my ($self, $c, $id) = @_;
+ my $guard = $c->model('DB')->txn_scope_guard;
+ {
+ my $preference = $self->require_preference($c);
+ last unless $preference;
+
+ my $json = $self->get_valid_patch_data(
+ c => $c,
+ id => $id,
+ media_type => 'application/json-patch+json',
+ );
+ last unless $json;
+
+ my $item = $self->item_by_id($c, $id);
+ last unless $self->resource_exists($c, voicemail => $item);
+ my $form = $self->get_form($c);
+ my $old_resource = $self->resource_from_item($c, $item, $form);
+ my $resource = $self->apply_patch($c, $old_resource, $json);
+ last unless $resource;
+
+ $item = $self->update_item($c, $item, $old_resource, $resource, $form);
+ last unless $item;
+
+ $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, $item, $form);
+ 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 PUT :Allow {
+ my ($self, $c, $id) = @_;
+ my $guard = $c->model('DB')->txn_scope_guard;
+ {
+ my $preference = $self->require_preference($c);
+ last unless $preference;
+
+ my $item = $self->item_by_id($c, $id);
+ last unless $self->resource_exists($c, voicemail => $item);
+ my $resource = $self->get_valid_put_data(
+ c => $c,
+ id => $id,
+ media_type => 'application/json',
+ );
+ last unless $resource;
+ my $form = $self->get_form($c);
+ my $old_resource = $self->resource_from_item($c, $item, $form);
+
+ $item = $self->update_item($c, $item, $old_resource, $resource, $form);
+ last unless $item;
+
+ $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, $item, $form);
+ 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 DELETE :Allow {
+ my ($self, $c, $id) = @_;
+
+ my $guard = $c->model('DB')->txn_scope_guard;
+ {
+ my $item = $self->item_by_id($c, $id);
+ last unless $self->resource_exists($c, voicemail => $item);
+
+ $item->delete;
+
+ $guard->commit;
+
+ $c->response->status(HTTP_NO_CONTENT);
+ $c->response->body(q());
+ }
+ return;
+}
+
+sub end : Private {
+ my ($self, $c) = @_;
+
+ $self->log_response($c);
+}
+
+# vim: set tabstop=4 expandtab:
diff --git a/lib/NGCP/Panel/Form/Voicemail/API.pm b/lib/NGCP/Panel/Form/Voicemail/API.pm
new file mode 100644
index 0000000000..83c8aa0014
--- /dev/null
+++ b/lib/NGCP/Panel/Form/Voicemail/API.pm
@@ -0,0 +1,86 @@
+package NGCP::Panel::Form::Voicemail::API;
+
+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' );
+sub build_render_list {[qw/submitid fields actions/]}
+sub build_form_element_class { [qw/form-horizontal/] }
+
+has_field 'pin' => (
+ type => 'Text',
+ label => 'PIN',
+ minlength => 4,
+ maxlength => 31,
+ required => 0,
+ element_attr => {
+ rel => ['tooltip'],
+ title => ['The PIN used to enter the IVR menu from external numbers.']
+ },
+);
+
+has_field 'email' => (
+ type => 'Email',
+ label => 'Email Address',
+ required => 0,
+ element_attr => {
+ rel => ['tooltip'],
+ title => ['The email address where to send notifications and the recordings.']
+ },
+)
+
+has_field 'delete' => (
+ type => 'Boolean',
+ label => 'Delete Messages',
+ required => 0,
+ element_attr => {
+ rel => ['tooltip'],
+ title => ['Delete voicemail recordings from the mailbox after delivering them via email.']
+ },
+);
+
+has_field 'attach' => (
+ type => 'Boolean',
+ label => 'Attach Recording',
+ required => 0,
+ element_attr => {
+ rel => ['tooltip'],
+ title => ['Attach recordings when delivering them via email. Must be set if delete flag is set']
+ },
+);
+
+has_field 'save' => (
+ type => 'Submit',
+ value => 'Save',
+ element_class => [qw/btn btn-primary/],
+ label => '',
+);
+
+has_block 'fields' => (
+ tag => 'div',
+ class => [qw/modal-body/],
+ render_list => [qw/pin email attach delete/],
+);
+
+has_block 'actions' => (
+ tag => 'div',
+ class => [qw/modal-footer/],
+ render_list => [qw/save/],
+);
+
+sub validate {
+ my $self = shift;
+ my $attach = $self->field('attach')->value;
+ my $delete = $self->field('delete')->value;
+ if($delete && !$attach) {
+ $self->field('attach')->add_error('Must be set if delete is set');
+ }
+}
+
+
+1;
+# vim: set tabstop=4 expandtab:
diff --git a/lib/NGCP/Panel/Form/Voicemail/Meta.pm b/lib/NGCP/Panel/Form/Voicemail/Meta.pm
new file mode 100644
index 0000000000..48005df5d5
--- /dev/null
+++ b/lib/NGCP/Panel/Form/Voicemail/Meta.pm
@@ -0,0 +1,97 @@
+package NGCP::Panel::Form::Voicemail::Meta;
+
+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' );
+sub build_render_list {[qw/submitid fields actions/]}
+sub build_form_element_class { [qw/form-horizontal/] }
+
+has_field 'subscriber_id' => (
+ type => 'PosInteger',
+ label => 'Subscriber ID',
+ required => 0,
+ element_attr => {
+ rel => ['tooltip'],
+ title => ['The subscriber id the message belongs to.']
+ },
+);
+
+has_field 'duration' => (
+ type => 'PosInteger',
+ label => 'Duration',
+ required => 0,
+ element_attr => {
+ rel => ['tooltip'],
+ title => ['The duration of the message.']
+ },
+);
+
+has_field 'time' => (
+ type => 'Text',
+ label => 'Time',
+ required => 0,
+ element_attr => {
+ rel => ['tooltip'],
+ title => ['The time the message was recorded.']
+ },
+);
+
+has_field 'caller' => (
+ type => 'Text',
+ label => 'Caller',
+ required => 0,
+ element_attr => {
+ rel => ['tooltip'],
+ title => ['The caller ID who left the message.']
+ },
+);
+
+has_field 'folder' => (
+ type => 'Select',
+ label => 'Folder',
+ required => 1,
+ element_attr => {
+ rel => ['tooltip'],
+ title => ['The folder the message is currently in (one of INBOX, Old, Work, Friends, Family, Cust1-Cust6)']
+ },
+ options => [
+ { label => 'INBOX', value => 'INBOX' },
+ { label => 'Old', value => 'Old' },
+ { label => 'Work', value => 'Work' },
+ { label => 'Friends', value => 'Friends' },
+ { label => 'Family', value => 'Family' },
+ { label => 'Cust1', value => 'Cust1' },
+ { label => 'Cust2', value => 'Cust2' },
+ { label => 'Cust3', value => 'Cust3' },
+ { label => 'Cust4', value => 'Cust4' },
+ { label => 'Cust5', value => 'Cust5' },
+ { label => 'Cust6', value => 'Cust6' },
+ ]
+);
+
+has_field 'save' => (
+ type => 'Submit',
+ value => 'Save',
+ element_class => [qw/btn btn-primary/],
+ label => '',
+);
+
+has_block 'fields' => (
+ tag => 'div',
+ class => [qw/modal-body/],
+ render_list => [qw/folder/],
+);
+
+has_block 'actions' => (
+ tag => 'div',
+ class => [qw/modal-footer/],
+ render_list => [qw/save/],
+);
+
+1;
+# vim: set tabstop=4 expandtab:
diff --git a/lib/NGCP/Panel/Role/API/VoicemailRecordings.pm b/lib/NGCP/Panel/Role/API/VoicemailRecordings.pm
new file mode 100644
index 0000000000..5e2665fc0b
--- /dev/null
+++ b/lib/NGCP/Panel/Role/API/VoicemailRecordings.pm
@@ -0,0 +1,41 @@
+package NGCP::Panel::Role::API::VoicemailRecordings;
+use Moose::Role;
+use Sipwise::Base;
+with 'NGCP::Panel::Role::API' => {
+ -alias =>{ item_rs => '_item_rs', },
+ -excludes => [ 'item_rs' ],
+};
+
+sub item_rs {
+ my ($self, $c) = @_;
+
+ my $item_rs = $c->model('DB')->resultset('voicemail_spool')->search({
+ duration => { '!=' => '' },
+ 'voip_subscriber.id' => { '!=' => undef },
+ },{
+ join => { mailboxuser => { provisioning_voip_subscriber => '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 => { mailboxuser => { provisioning_voip_subscriber => { voip_subscriber => { contract => 'contact' } } } }
+ });
+ }
+ return $item_rs;
+}
+
+sub get_form {
+ my ($self, $c) = @_;
+ return;
+}
+
+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:
diff --git a/lib/NGCP/Panel/Role/API/Voicemails.pm b/lib/NGCP/Panel/Role/API/Voicemails.pm
new file mode 100644
index 0000000000..a1df38bb54
--- /dev/null
+++ b/lib/NGCP/Panel/Role/API/Voicemails.pm
@@ -0,0 +1,112 @@
+package NGCP::Panel::Role::API::Voicemails;
+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 NGCP::Panel::Form::Voicemail::Meta;
+
+sub item_rs {
+ my ($self, $c) = @_;
+
+ my $item_rs = $c->model('DB')->resultset('voicemail_spool')->search({
+ duration => { '!=' => '' },
+ 'voip_subscriber.id' => { '!=' => undef },
+ },{
+ join => { mailboxuser => { provisioning_voip_subscriber => '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 => { mailboxuser => { provisioning_voip_subscriber => { voip_subscriber => { contract => 'contact' } } } }
+ });
+ }
+ return $item_rs;
+}
+
+sub get_form {
+ my ($self, $c) = @_;
+ return NGCP::Panel::Form::Voicemail::Meta->new;
+}
+
+sub hal_from_item {
+ my ($self, $c, $item, $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)),
+ Data::HAL::Link->new(relation => 'ngcp:subscribers', href => sprintf("/api/subscribers/%d", $item->mailboxuser->provisioning_voip_subscriber->voip_subscriber->id)),
+ Data::HAL::Link->new(relation => 'ngcp:voicemailrecordings', href => sprintf("/api/voicemailrecordings/%d", $item->id)),
+ ],
+ relation => 'ngcp:'.$self->resource_name,
+ );
+
+ my $resource = $self->resource_from_item($c, $item, $form);
+ $hal->resource($resource);
+ return $hal;
+}
+
+sub resource_from_item {
+ my ($self, $c, $item, $form) = @_;
+
+ $form //= $self->get_form($c);
+
+ my %resource = ();
+ $resource{id} = int($item->id);
+ $resource{duration} = $item->duration->is_int ? int($item->duration) : 0;
+ $resource{time} = "" . $item->origtime;
+ $resource{caller} = $item->callerid;
+ $resource{subscriber_id} = int($item->mailboxuser->provisioning_voip_subscriber->voip_subscriber->id);
+
+ # type is last item of path like /var/spool/asterisk/voicemail/default/uuid/INBOX
+ my @p = split '/', $item->dir;
+ $resource{folder} = pop @p;
+
+ return \%resource;
+}
+
+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) = @_;
+
+ $form //= $self->get_form($c);
+ return unless $self->validate_form(
+ c => $c,
+ form => $form,
+ resource => $resource,
+ );
+
+ my $f = $resource->{folder};
+ my $upresource = {};
+ $upresource->{dir} = $item->dir;
+ $upresource->{dir} =~ s/\/[^\/]+$/\/$f/;
+
+ $item->update($upresource);
+
+ return $item;
+}
+
+1;
+# vim: set tabstop=4 expandtab:
diff --git a/share/templates/api/root/collection.tt b/share/templates/api/root/collection.tt
index b5aca755d8..2441eaf723 100644
--- a/share/templates/api/root/collection.tt
+++ b/share/templates/api/root/collection.tt
@@ -12,6 +12,31 @@
[% col.description %]
+
[% @@ -55,7 +81,9 @@ Accept-Post: application/hal+json; profile="http://purl.org/sipwise/ngcp-api/#re INCLUDE helpers/api_req_res.tt request=request response=response level=level+3; -%]
+[% END -%] +[% IF col.actions.grep('^GET$').size -%]
@@ -144,7 +172,9 @@ Content-Type: application/hal+json; profile="http://purl.org/sipwise/ngcp-api/" INCLUDE helpers/api_req_res.tt request=request response=response level=level+3; -%]
+[% END -%] +[% IF col.item_actions.grep('^GET$').size -%]@@ -203,7 +233,9 @@ Link: ; rel="item self" INCLUDE helpers/api_req_res.tt request=request response=response level=level+3; -%]
+[% END -%] +[% IF col.actions.grep('^POST$').size -%][% @@ -262,7 +294,9 @@ Location: /api/' _ id _ '/2'; INCLUDE helpers/api_req_res.tt request=request response=response level=level+3; %]
+[% END -%] +[% IF col.item_actions.grep('^PUT$').size -%][% @@ -322,7 +356,9 @@ Preference-Applied: return=minimal'; INCLUDE helpers/api_req_res.tt request=request response=response level=level+3; -%]
+[% END -%] +[% IF col.item_actions.grep('^PATCH$').size -%][% @@ -442,6 +478,25 @@ Preference-Applied: return=minimal'; INCLUDE helpers/api_req_res.tt request=request response=response level=level+3; -%]
+[% END -%] + +[% IF col.item_actions.grep('^DELETE$').size -%] ++ +[% + cmd = 'curl -i -X DELETE -H \'Connection: close\' --cert NGCP-API-client-certificate.pem --cacert ca-cert.pem \'https://example.org:1443/api/' _ id _ '/1\''; + INCLUDE helpers/api_command.tt cmd=cmd level=level+3; + + request = +'DELETE /api/' _ id _ '/1 HTTP/1.1'; + response = +'HTTP/1.1 204 No Content'; + + INCLUDE helpers/api_req_res.tt request=request response=response level=level+3; +-%] +
+[% END -%]