TT#13277 Implement panel interface for call rec

Allows you to see/delete recorded calls and listen to the streams.

Change-Id: I339c81194efa8a77ee390566bedb46c59c83e671
changes/97/12197/4
Andreas Granig 9 years ago
parent 052e776317
commit 9f168936a8

@ -8,6 +8,7 @@ use JSON qw(decode_json encode_json);
use URI::Escape qw(uri_unescape);
use Data::Dumper;
use MIME::Base64 qw(encode_base64url decode_base64url);
use File::Slurp qw/read_file/;
use NGCP::Panel::Utils::Navigation;
use NGCP::Panel::Utils::Contract;
@ -2245,6 +2246,11 @@ sub master :Chained('base') :PathPart('details') :CaptureArgs(0) {
{ name => "origtime", search_from_epoch => 1, search_to_epoch => 1, title => $c->loc('Time') },
{ name => "duration", search => 1, title => $c->loc('Duration') },
]);
$c->stash->{streams_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [
{ name => "id", search => 1, title => $c->loc('#') },
{ name => "output_type", title => $c->loc('Type') },
{ name => "file_format", title => $c->loc('Format') },
]);
$c->stash->{reg_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [
{ name => "id", search => 1, title => $c->loc('#') },
#left untouchable, although user_agent is always the same by design, see MT 14789 notes
@ -2257,6 +2263,15 @@ sub master :Chained('base') :PathPart('details') :CaptureArgs(0) {
{ name => "call_id", search => 1, title => $c->loc('Call-ID') },
{ name => "cseq_method", search => 1, title => $c->loc('Method') },
]);
my $rec_cols = [
{ name => "id", search => 1, title => $c->loc('#') },
{ name => "start_timestamp", search_from_epoch => 1, search_to_epoch => 1, title => $c->loc('Time') },
];
if($c->user->roles eq "admin" || $c->user->roles eq "reseller") {
push @{ $rec_cols },
{ name => "call_id", title => $c->loc('Call-ID') };
}
$c->stash->{rec_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, $rec_cols);
$c->stash(
template => 'subscriber/master.tt',
@ -2293,10 +2308,21 @@ sub voicemails :Chained('master') :PathPart('voicemails') :Args(0) {
);
}
sub recordings :Chained('master') :PathPart('recordings') :Args(0) {
my ($self, $c) = @_;
$c->stash(
template => 'subscriber/recording.tt'
);
}
sub calllist_master :Chained('base') :PathPart('calls') :CaptureArgs(0) {
my ($self, $c) = @_;
$c->stash->{callid_enc} = $c->req->params->{callid};
if($c->stash->{callid_enc}) {
$c->stash->{callid} = decode_base64url($c->stash->{callid_enc});
}
my $call_cols = [
{ name => "id", title => $c->loc('#') },
{ name => "direction", search => 1, literal_sql => 'if(source_user_id = "'.$c->stash->{subscriber}->uuid.'", "outgoing", "incoming")' },
@ -3526,12 +3552,14 @@ sub _process_calls_rows {
sub ajax_calls :Chained('calllist_master') :PathPart('list/ajax') :Args(0) {
my ($self, $c) = @_;
my $callid = $c->stash->{callid};
my $out_rs = $c->model('DB')->resultset('cdr')->search({
source_user_id => $c->stash->{subscriber}->uuid,
($callid ? (call_id => $callid) : ()),
});
my $in_rs = $c->model('DB')->resultset('cdr')->search({
destination_user_id => $c->stash->{subscriber}->uuid,
($callid ? (call_id => $callid) : ()),
});
my $rs = $out_rs->union_all($in_rs);
@ -3605,6 +3633,40 @@ sub ajax_voicemails :Chained('master') :PathPart('voicemails/ajax') :Args(0) {
$c->detach( $c->view("JSON") );
}
sub ajax_recordings :Chained('master') :PathPart('recordings/ajax') :Args(0) {
my ($self, $c) = @_;
my $rec_rs = $c->model('DB')->resultset('recording_calls')->search({
'status' => { -in => ['completed', 'confirmed'] },
'recording_metakeys.key' => 'uuid',
'recording_metakeys.value' => $c->stash->{subscriber}->uuid,
}, {
join => 'recording_metakeys'
});
NGCP::Panel::Utils::Datatables::process($c, $rec_rs, $c->stash->{rec_dt_columns},
sub {
my $item = shift;
my %result;
$result{call_id} = $item->call_id =~ s/(_b2b-1|_pbx-1)+$//r;
# similar to Utils::CallList::process_cdr_item
$result{call_id_url} = encode_base64url($result{call_id});
return %result;
}
);
$c->detach( $c->view("JSON") );
}
sub ajax_recording_streams :Chained('recording') :PathPart('streams/ajax') :Args(0) {
my ($self, $c) = @_;
my $rs = $c->stash->{recording}->recording_streams;
NGCP::Panel::Utils::Datatables::process($c, $rs, $c->stash->{streams_dt_columns});
$c->detach( $c->view("JSON") );
}
sub ajax_captured_calls :Chained('master') :PathPart('callflow/ajax') :Args(0) {
my ($self, $c) = @_;
@ -3703,6 +3765,97 @@ sub delete_voicemail :Chained('voicemail') :PathPart('delete') :Args(0) {
$c->uri_for_action('/subscriber/details', [$c->req->captures->[0]]));
}
sub recording :Chained('master') :PathPart('recording') :CaptureArgs(1) {
my ($self, $c, $rec_id) = @_;
my $rs = $c->model('DB')->resultset('recording_calls')->search({
'me.id' => $rec_id,
'recording_metakeys.key' => 'uuid',
'recording_metakeys.value' => $c->stash->{subscriber}->uuid,
},{
join => ['recording_streams', 'recording_metakeys'],
});
unless($rs->first) {
NGCP::Panel::Utils::Message::error(
c => $c,
log => "no such recording with id '$rec_id' for uuid ".$c->stash->{subscriber}->uuid,
desc => $c->loc('No such recording'),
);
NGCP::Panel::Utils::Navigation::back_or($c,
$c->uri_for_action('/subscriber/details', [$c->req->captures->[0]]));
}
$c->stash->{recording} = $rs->first;
}
sub recording_streams :Chained('recording') :PathPart('streams') :Args(0) {
my ($self, $c) = @_;
$c->stash(
template => 'subscriber/recording_streams.tt'
);
}
sub recording_stream :Chained('recording') :PathPart('streams') :CaptureArgs(1) {
my ($self, $c, $stream_id) = @_;
my $stream = $c->stash->{recording}->recording_streams->find($stream_id);
unless($stream) {
NGCP::Panel::Utils::Message::error(
c => $c,
log => "no such recording with id '$stream_id' for recording id ".$c->stash->{recording}->id,
desc => $c->loc('No such recording file'),
);
NGCP::Panel::Utils::Navigation::back_or($c,
$c->uri_for_action('/subscriber/details', [$c->req->captures->[0]]));
}
$c->stash->{stream} = $stream;
}
sub play_stream :Chained('recording_stream') :PathPart('play') :Args(0) {
my ($self, $c) = @_;
# TODO: fix to be able to select certain stream
my $stream = $c->stash->{stream};
my $data = read_file($stream->full_filename);
my $mime_type;
if($stream->file_format eq "wav") {
$mime_type = 'audio/x-wav';
} elsif($stream->file_format eq "mp3") {
$mime_type = 'audio/mpeg';
} else {
$mime_type = 'application/octet-stream';
}
$c->response->header('Content-Disposition' => 'attachment; filename="call-recording-'.$stream->id.'.'.$stream->file_format.'"');
$c->response->content_type($mime_type);
$c->response->body($data);
}
sub delete_recording :Chained('recording') :PathPart('delete') :Args(0) {
my ($self, $c) = @_;
$c->detach('/denied_page')
if(($c->user->roles eq "admin" || $c->user->roles eq "reseller") && $c->user->read_only);
try {
$c->stash->{recording}->delete;
NGCP::Panel::Utils::Message::info(
c => $c,
data => { $c->stash->{recording}->get_inflated_columns },
desc => $c->loc('Successfully deleted recording'),
);
} catch($e) {
NGCP::Panel::Utils::Message::error(
c => $c,
error => $e,
desc => $c->loc('Failed to delete recording'),
);
}
NGCP::Panel::Utils::Navigation::back_or($c,
$c->uri_for_action('/subscriber/details', [$c->req->captures->[0]]));
}
sub registered :Chained('master') :PathPart('registered') :CaptureArgs(1) {
my ($self, $c, $reg_id) = @_;

@ -108,25 +108,37 @@ $(document).ready(function() {
{ "mRender": function ( data, type, full ) {
var html = '' +
'<div class="sw_actions pull-right">';
[% backuri = backuri | uri%]
[% FOR button IN helper.dt_buttons -%]
[% confirm_delete = button.name == c.loc("Delete") ? 'data-confirm="Delete"' : '' -%]
[% confirm_delete = button.name == c.loc("Terminate") ? 'data-confirm="Terminate"' : confirm_delete -%]
[%IF !( button.uri.search('[\?\&]back=|^javascript:') ); %]
[% button.uri = button.uri _ '?back=' _ backuri %]
[%END%]
[% IF button.condition -%]
if([% button.condition %]) {
[% END -%]
[%
backuri = backuri | uri;
FOR button IN helper.dt_buttons;
separator = '?';
confirm_delete = button.name == c.loc("Delete") ? 'data-confirm="Delete"' : '';
confirm_delete = button.name == c.loc("Terminate") ? 'data-confirm="Terminate"' : confirm_delete;
IF button.uri.search('\?');
separator = '&';
END;
IF !( button.uri.search('[\?\&]back=|^javascript:') );
button.uri = button.uri _ separator _ 'back=' _ backuri;
END;
IF button.condition;
-%]
if([% button.condition %]) {
[%
END;
-%]
html +=
'<a class="btn [% button.class %]" href="[% button.uri %]" [% confirm_delete %][%IF button.onclick -%] onclick="[%button.onclick%]"[%END%][%IF button.tooltip -%] rel="tooltip" data-original-title="[%button.tooltip%]"[%END%] [% button.attributes %]>' +
'<i class="[% button.icon %]"></i> [% button.name %]' +
'</a>';
[% IF button.condition -%]
[%
IF button.condition;
-%]
}
[% END -%]
[% END -%]
[%
END;
END;
-%]
html += '</div>';
return html;
},

@ -31,9 +31,9 @@ function showCallDetails(callId){
<select onChange="changeCalllist(this);" id="Calls_table_external_filter">
<option value="[% c.uri_for_action('/subscriber/ajax_calls', [c.req.captures.0]) -%]">Show all calls</option>
<option value="[% c.uri_for_action('/subscriber/ajax_calls_out', [c.req.captures.0]) -%]">Show outgoing calls only</option>
<option value="[% c.uri_for_action('/subscriber/ajax_calls_in', [c.req.captures.0]) -%]">Show incoming calls only</option>
<option value="[% c.uri_for_action('/subscriber/ajax_calls', [c.req.captures.0], undef, {callid => callid}) -%]">Show all calls</option>
<option value="[% c.uri_for_action('/subscriber/ajax_calls_out', [c.req.captures.0], undef, {callid => callid}) -%]">Show outgoing calls only</option>
<option value="[% c.uri_for_action('/subscriber/ajax_calls_in', [c.req.captures.0], undef, {callid => callid}) -%]">Show incoming calls only</option>
</select>
<script>
function changeCalllist(selection) {
@ -52,7 +52,7 @@ function changeCalllist(selection) {
helper.length_change = 1;
helper.column_sort = 'start_time';
helper.form_object = form;
helper.ajax_uri = c.uri_for_action('/subscriber/ajax_calls', [c.req.captures.0]);
helper.ajax_uri = c.uri_for_action('/subscriber/ajax_calls', [c.req.captures.0], undef, {callid => callid_enc});
helper.show_footer = 1;
IF (c.user.roles == "admin" || c.user.roles == "reseller");

@ -225,6 +225,44 @@ function process_pbx_items(moveId,direction){
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" data-parent="#subscriber_data" href="#collapse_recordings">[% c.loc('Call Recordings') %]</a>
</div>
<div class="accordion-body collapse" id="collapse_recordings">
<div class="accordion-inner">
[%
helper.name = c.loc('Call Recordings');
helper.identifier = 'recordings';
helper.column_sort = 'id';
helper.dt_columns = rec_dt_columns;
helper.form_object = form;
helper.ajax_uri = c.uri_for_action('/subscriber/ajax_recordings', [c.req.captures.0]);
IF ((c.user.roles == "admin" || c.user.roles == "reseller") && !c.user.read_only) || c.user.roles == "subscriber" || c.user.roles == "subscriberadmin";
helper.dt_buttons = [
{ name = c.loc('Call Details'), uri = "/subscriber/" _ subscriber.id _ "/calls?callid=' + encodeURIComponent(full.call_id_url) + '", class = 'btn-small btn-tertiary', icon = 'icon-search' },
{ name = c.loc('Recorded Files'), uri = "details/recording/'+full.id+'/streams", class = 'btn-small btn-tertiary', icon = 'icon-play' },
{ name = c.loc('Delete'), uri = "details/recording/'+full.id+'/delete", class = 'btn-small btn-secondary', icon = 'icon-trash' },
];
ELSE;
helper.dt_buttons = [
{ name = c.loc('Call Details'), uri = "/subscriber/" _ subscriber.id _ "/calls?callid=' + encodeURIComponent(full.call_id_url) + '", class = 'btn-small btn-tertiary', icon = 'icon-search' },
{ name = c.loc('Files'), uri = "details/recording/'+full.id+'/streams", class = 'btn-small btn-tertiary', icon = 'icon-play' },
];
END;
PROCESS 'helpers/datatables.tt';
%]
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">

@ -0,0 +1,30 @@
[% site_config.title = c.loc('Call Recordings for [_1]@[_2]', subscriber.username, subscriber.domain.domain) -%]
<div class="row">
<span class="pull-left" style="margin:0 5px 0 5px;">
<a class="btn btn-primary btn-large" href="[% c.uri_for('/back') %]"><i class="icon-arrow-left"></i> [% c.loc('Back') %]</a>
</span>
</div>
[% back_created = 1 -%]
<div class="ngcp-separator"></div>
[%
helper.name = c.loc('Call Recordings');
helper.column_sort = 'origtime';
helper.dt_columns = rec_dt_columns;
helper.form_object = form;
helper.ajax_uri = c.uri_for_action('/subscriber/ajax_recordings', [c.req.captures.0]);
helper.dt_buttons = [
{ name = c.loc('Call Details'), uri = "/subscriber/" _ subscriber.id _ "/calls?callid=' + encodeURIComponent(full.call_id_url) + '", class = 'btn-small btn-tertiary', icon = 'icon-search' },
{ name = c.loc('Recorded Files'), uri = "recording/'+full.id+'/streams", class = 'btn-small btn-tertiary', icon = 'icon-play' },
{ name = c.loc('Delete'), uri = "recording/'+full.id+'/delete", class = 'btn-small btn-secondary', icon = 'icon-trash' },
];
PROCESS 'helpers/datatables.tt';
%]
[% # vim: set tabstop=4 syntax=html expandtab: -%]

@ -0,0 +1,28 @@
[% site_config.title = c.loc('Recorded Files') -%]
<div class="row">
<span class="pull-left" style="margin:0 5px 0 5px;">
<a class="btn btn-primary btn-large" href="[% c.uri_for('/back') %]"><i class="icon-arrow-left"></i> [% c.loc('Back') %]</a>
</span>
</div>
[% back_created = 1 -%]
<div class="ngcp-separator"></div>
[%
helper.name = c.loc('Recorded files for this call');
helper.column_sort = 'id';
helper.dt_columns = streams_dt_columns;
helper.form_object = form;
helper.ajax_uri = c.uri_for_action('/subscriber/ajax_recording_streams', c.req.captures);
helper.dt_buttons = [
{ name = c.loc('Play'), uri = "streams/'+full.id+'/play", class = 'btn-small btn-tertiary', icon = 'icon-play' },
];
PROCESS 'helpers/datatables.tt';
%]
[% # vim: set tabstop=4 syntax=html expandtab: -%]

@ -7,6 +7,7 @@
<ul class="dropdown-menu">
<li><a href="[% c.uri_for_action('/subscriber/calllist', [c.user.voip_subscriber.id]) %]">[% c.loc('Call List') %]</a></li>
<li><a href="[% c.uri_for_action('/subscriber/voicemails', [c.user.voip_subscriber.id]) %]">[% c.loc('Voicebox Messages') %]</a></li>
<li><a href="[% c.uri_for_action('/subscriber/recordings', [c.user.voip_subscriber.id]) %]">[% c.loc('Call Recordings') %]</a></li>
<li><a href="[% c.uri_for_action('/subscriber/reglist', [c.user.voip_subscriber.id]) %]">[% c.loc('Registered Devices') %]</a></li>
<li><a href="[% c.uri_for_action('/subscriber/preferences', [c.user.voip_subscriber.id]) %]">[% c.loc('Subscriber Settings') %]</a></li>
<li><a href="[% c.uri_for_action('/subscriber/webpass', [c.user.voip_subscriber.id]) %]">[% c.loc('User Details') %]</a></li>

Loading…
Cancel
Save