Tag Archives: PHP

Command-line mailbox downloader for Cisco Unity Connection

Starting in version 8 of Cisco Unity Connection, it is simple to write scripts to manipulate various aspects of Unity Connection, including user attributes (password/PIN, for example), messages, call handlers, and more, thanks to a new REST API. There are interfaces to both administrative and end-user functions. And not only scripts, but custom web portals or other web integrations. No SOAP and AXL required!

See Cisco’s DocWiki for the details: http://docwiki.cisco.com/wiki/Cisco_Unity_Connection_APIs
And read the rest of this entry for a simple PHP script that connects to a user’s mailbox, dumps all the contents to a local folder, writes out a table of contents and gzips the whole thing up.
#!/usr/bin/php <?php if ($argc !== 3) {   fwrite(STDERR, "Usage: n");   fwrite(STDERR, "  php $argv[0] <mailbox> <password>n");   exit(1); } /* Web connection setup */ $baseURL = "https://YOUR-UCONN-SERVER-HERE:8443"; $auth = base64_encode("$argv[1]:$argv[2]"); $opts = array( 'http' => array ('method' => 'GET',                                 'timeout' => 4,                                 'header' => "Authorization: Basic $auth"),                'ssl' =>  array ('allow_self_signed' => TRUE)              ); $context = stream_context_create($opts); /* Get the user's folders list and counts */ $foldersURL = "$baseURL/vmrest/mailbox/folders"; if (!($result = file_get_contents($foldersURL, false, $context))) {   fwrite(STDERR, "Could not connect to Unity Connection for mailbox folders list.n");   exit(1); } $folders = simplexml_load_string($result); /* Now get contents of each mailbox that has messages */ $currtime = datestring(time()); $outputdir = $argv[1] . "_$currtime"; if (!mkdir($outputdir)) {   fwrite(STDERR, "Couldn't create directory $outputdirn");   exit(1); } $INDEX = fopen("$outputdir/index.txt", 'w'); $mainheader = "Mailbox of $argv[1] downloaded at $currtime"; fwrite($INDEX, "$mainheadern" . sprintf("%'=" . strlen($mainheader) . "snn", "")); foreach ($folders as $folder) {   if ($folder->MessageCount > 0) {     $folderheader = "Folder: $folder->DisplayName";     fwrite($INDEX, "$folderheadern" . sprintf("%'-" . strlen($folderheader) . "snn", ""));     $messagedir = $outputdir . "/" . $folder->DisplayName;     mkdir($messagedir);     $result = file_get_contents($baseURL . $folder->URI . "/messages", false, $context);     $xml = simplexml_load_string($result);     $counter = 1;     foreach ($xml->Message as $message) {       fwrite($INDEX, "$counter. Message from ". ($message->CallerId->CallerName == ""?"(no name listed)":$message->CallerId->CallerName));       fwrite($INDEX, " at " . ($message->CallerId->CallerNumber == ""?"(no number listed)":$message->CallerId->CallerNumber));       fwrite($INDEX, "n");       fwrite($INDEX, "Listened: " . $message->Read . "n");       fwrite($INDEX, "Length: " . $message->Duration/1000 . " secondsn");       fwrite($INDEX, "Timestamp:" . datestring($message->ArrivalTime/1000) . "n");       fwrite($INDEX, "Filename: ");       $filename = datestring($message->ArrivalTime/1000) . "_from_" . $message->CallerId->CallerNumber . ".wav";       fwrite($INDEX, "$filenamenn");       $innerResult = file_get_contents($baseURL . $message->URI, false, $context);       $messageContainer = simplexml_load_string($innerResult);       $audio = file_get_contents($baseURL . $messageContainer->Attachments->Attachment->URI, false, $context);       if (!file_put_contents("$messagedir/$filename", $audio, LOCK_EX))         fwrite(STDERR, "Error writing audio to file: $filenamen");       $counter++;     }     fwrite($INDEX, "n");   } } fclose($INDEX); $tarresult = system("tar czf $outputdir.tar.gz $outputdir"); if ($tarresult !== FALSE) {   system("rm -rf $outputdir"); } else {   fwrite(STDERR, "Unable to create tar.gz file, but the folder is still there.n"); } fwrite(STDERR, "Mailbox dump completed successfully.n"); exit(0); function datestring ($unixtime) {   return date('Y-m-d_H-i-s', $unixtime); } ?>

Modify FreePBX call reports to show destination channel

FreePBX‘s Reports module does not show the destination channel–for example, the outbound trunk, or the device that received an incoming call–by default. But you can customize the call log display without editing the module itself. The Reports module looks for a file at /etc/asterisk/call-log-table.php, and if it finds the file, uses the declarations within to redefine the column layout. I took the default and modified it to include the Destination Channel column.

Just create a new file at /etc/asterisk/call-log-table.php with the following contents:

<?php   $FG_TABLE_COL[]=array ("Timestamp", "calldate", "12%", "center", "SORT", "19");
        $FG_TABLE_COL[]=array ("Src Chan", "channel", "14%", "center", "", "30", "",
"", "", "", "", "display_acronym");
        $FG_TABLE_COL[]=array ("Source", "src", "10%", "center", "", "30");
        $FG_TABLE_COL[]=array ("CLID", "clid", "25%", "center", "", "80",'','','','',
'','filter_html');
        $FG_TABLE_COL[]=array ("Dest", "dst", "10%", "center", "SORT", "30");
        $FG_TABLE_COL[]=array ("Dst Chan", "dstchannel", "14%", "center", "", "30",
"", "", "", "", "", "display_acronym");
        $FG_TABLE_COL[]=array ("Disposition", "disposition", "9%", "center", "", "30");
        if ((!isset($resulttype)) || ($resulttype=="min"))
$minute_function= "display_minute";
        $FG_TABLE_COL[]=array ("Duration", "duration", "6%", "center", "SORT", "30",
"", "", "", "", "", "$minute_function");

        $FG_TABLE_DEFAULT_ORDER = "calldate";
        $FG_TABLE_DEFAULT_SENS = "DESC";

        // This Variable stores the argument for the SQL query
        $FG_COL_QUERY='calldate, channel, src, clid, dst, dstchannel, disposition,
duration';
        $FG_COL_QUERY_GRAPH='calldate, duration';

        // The variable LIMITE_DISPLAY define the limit of record to display by page
        $FG_LIMITE_DISPLAY=25;

        // Number of column in the html table
        $FG_NB_TABLE_COL=count($FG_TABLE_COL);

        // The variable $FG_EDITION define if you want process to the edition of the
// database record
        $FG_EDITION=true;

        //This variable will store the total number of columns
        $FG_TOTAL_TABLE_COL = $FG_NB_TABLE_COL;
        if ((isset($FG_DELETION) && $FG_DELETION) || $FG_EDITION)
$FG_TOTAL_TABLE_COL++;

        //This variable define the Title of the HTML table
        $FG_HTML_TABLE_TITLE=" - Call Logs - ";

        //This variable define the width of the HTML table
        $FG_HTML_TABLE_WIDTH="100%";
?>

Appendix: analog remix debugging and notes

Part 1: Intro, Part 2: Tropo/PHP, Part 3: Asterisk Manager/Perl

These notes are from a weekend of hacking and debugging with Michigan Telephone, the first guinea pig to try out the “analog remix” scripts. Thanks for the extra challenges. 🙂

Asterisk 1.4, 1.8 and the Asterisk Manager Interface

  • Asterisk 1.4’s Manager calls the channel that is bridged to the current channel “Link.” Newer versions, more sensibly, call it “BridgedChannel.” I updated the Perl CGI to use whichever is defined, so now the CGI will work with newer versions of Asterisk, and not just 1.4.
  • A bug exists on specific versions of Asterisk 1.4, 1.6 and 1.8 that breaks call transfer from the Manager (as well as SIP blind transfers). The current -rc1 versions, as of this writing, have the fix: 1.4.39-rc1, 1.6.2.16-rc1, and 1.8.2-rc1. This bug has been in 1.4 since 1.4.38-rc1; in 1.6 since 1.6.2.15-rc1; and in 1.8 since 1.8.0-rc4. If you’re on a buggy version, the transfer will just hang up the channel.
  • How to set up the Manager user may not be obvious. Just make a new user block in manager.conf or manager_custom.conf (FreePBX) with the following permissions:

    [tropousername]
    secret = tropopassword
    deny=0.0.0.0/0.0.0.0
    permit=127.0.0.1/255.255.255.0
    read = call
    write = call

    Set the username and password to whatever you want, and then use these in the Perl CGI in the AMI::Common::new() function. Your manager user needs only “call” permissions to perform the actions in the CGI.

Perl

  • MT notes that you may have to install the AnyEvent and parent Perl modules before Asterisk::AMI. Use CPAN’s installer if you can: perl -MCPAN -e shell and install parent, AnyEvent, and Asterisk::AMI
  • The Data::Dumper module can be really helpful if you find that things aren’t going well with your CGI. Just use Data::Dumper at the top of the CGI, then print STDERR Dumper($variablename) wherever you need it. Try this on a hash, such as $currchan, and you’ll get a dump of the whole thing.

Firewall/ACL

You may want to set up a separate Apache VirtualHost with access controls (username/password and/or IP access list) for your Tropo CGI. Or, if your web server is firewalled, you’ll need to let Tropo in to make the request. Tropo’s support team indicated that requests will come from 66.193.54.21, so this IP can be allowed through your firewall. They will announce any changes to this on changes.tropo.com.

Analog remix part 2: Tropo

Tropo, a project of Voxeo Corporation, adds voice and short-message-communications functions to PHP, JavaScript, Ruby, Python, and Groovy through a set of classes. They host the platform and your scripts, which are then triggered through the web or by PSTN/SIP, SMS, Twitter, or chat events. You can use the system as long as you want for free for development, or pay reasonable hosting rates to go production. Oh, and their support team is terrific, even for developers who haven’t yet paid them a dime.
So what can you do with it?
  • Interactive voice-response (IVR) systems
  • Chat or Twitter robot
  • Dialer
  • Call routing based on logic in the script
  • Text-to-speech
  • Speech-to-text
  • Audio recording
  • Whatever you can normally do with the aforementioned scripting languages, but note that the stdout will be a communications outlet such as a SIP channel, a Twitter feed or conversation, a chat, or an SMS–not a web page
My first Tropo application, the “local operator,” is the topic of part two of the analog remix. The platform has so many features, though, that I look forward to spending more time developing in it.
I chose to use PHP because it’s the language with which I am most familiar and also has Curl libraries built in, which I am using to make a CGI callback to my own Asterisk server. The purpose should become clear when you read the code, but you may ask, “why not just send a SIP REFER to do the call transfer?” The answer is that you can’t with the Tropo platform. All transfers go through Tropo; there’s no option for a REFER. So if you want to use your own dialplan and not have your call going out to Tropo and back to use local resources, you have to do the transfer through the Asterisk Manager, thus the CGI call.
Number, Please?
The script answers the call (which will come in by SIP URI), asks, “Number, please?”, validates and confirms the spoken number with the caller, then uses the Curl functions to go back to the calling Asterisk server (running a web server) and transfer the call.
Getting There From Asterisk
Once you’ve written a script and uploaded it to  Tropo (or typed it directly into Tropo’s file-editing interface), you go to the Applications section and choose your script as the source. Once you’ve submitted it, you’ll be given a SIP URI (amongst other methods) by which to access the application. So as part of a FreePBX Custom Extension or in your extensions.conf file, you can make an extension for your new application by referencing SIP/somenumber@sip.tropo.com. I set mine up as extension 1100, which I mentioned in the previous post.
Now, if I go off hook from one of my internal extensions and dial 1100, or simply pick up the analog extension on which I configured the Warm Line/Hot Line, I’ve got the operator ready to take my number and transfer my call. Only one piece left after this: the Asterisk Manager CGI script.

Appendix: the PHP code (Number, Please?)

<?php

// Main

// SSML tags needed for advanced text-to-speech manipulation
$ssml_start = "<?xml version='1.0'?><speak>";
$sayas_end="</say-as>";
$sayas_digits ="<say-as interpret-as='vxml:digits'>";
$ssml_end = "</speak>";

answer();

// These IDs are used to identify the caller and the Asterisk box
$callid = $currentCall->getHeader('x-sbc-call-id');
$hostid = trim((strstr($callid, '@')),'@');

_log("Call-ID is $callid");
_log("Host is $hostid");

wait(1000);

$haveNumber = false;

while (!$haveNumber) {
$number = ask($ssml_start . "Number <emphasis level='reduced'>please?</emphasis>" .
$ssml_end,
array(voice => "allison",
attempts => 3,
bargein => false,
choices => "[4-16 DIGITS]",
minConfidence => 0.4,
timeout => 8,
onBadChoice => "handlerBadChoice",
onError => "handlerError",
onHangup => "handlerHangup",
onTimeout => "handlerTimeout"
));

// Confirm the number with the caller
// 1 and 0 are options for testing with DTMF
$yesno = ask($ssml_start . $sayas_digits . $number->value . $sayas_end .
", is this correct?" . $ssml_end,
array(voice => "allison",
attempts => 2,
bargein => false,
choices => "yes, no, 1, 0",
minConfidence => 0.5,
timeout => 3,
onBadChoice => "handlerBadChoice",
onError => "handlerError",
onHangup => "handlerHangup",
onTimeout => "handlerTimeout"
));

_log("Caller said $yesno->value.");

if ($yesno->value == "yes" || $yesno->value == "1") {
$haveNumber = true;
} else {
say("OK, let's try again.");
}
}

// The CGI callback. Refer to a future posting for the design
// Customize for your server. Use SSL for security if you can.
$url = "http://$hostid/tropo.cgi?f=xfer&callid=" . urlencode($callid) .
"&dest=" . $number->value;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "USERNAME:PASSWORD");
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);

_log("Received from CGI: " . $result = curl_exec($ch));

if (!$result) {
_log(curl_error($ch));
}
curl_close($ch);

// If the transfer didn't happen, the caller is still here...

if ($currentCall->isActive()) {
_log("Transfer failed and caller still on the line.");
say("I'm sorry, something went wrong. Please try again later.");
hangup();
}

return;
// END OF MAIN

// Function definitions

function handlerBadChoice($event) {
say("I'm sorry, I didn't understand you.");
if($event->attempt >= 3) {
say("Goodbye.");
hangup();
}
return;
}

function handlerError($event) {
_log("Error occurred: $event->value");
say("I'm sorry, an error occurred. Goodbye");
hangup();
return;
}

function handlerHangup($event) {
_log("Caller hung up.");
exit;
}

function handlerTimeout($event) {
say("I'm sorry, I didn't hear you.");
if($event->attempt >= 3) {
say("Goodbye.");
hangup();
}
return;
}

?>