Telephone scripts

From Hackerspace ACKspace
Revision as of 15:31, 23 June 2012 by Xopr (talk | contribs) (added closing announcement paging system)
Jump to: navigation, search

Number lookup

We make use of the mod_cidlookup for ease of use, but not all features are functional in our implementation. Every request is done via HTTP requests to a php script that will check if the number:

  • is an extension (and returns the name for that)
  • is stored in the local mySQL database, and return that
  • can be found online by using reversed number lookup websites for landlines
  • can be found online by using the national telecommunications authority database (opta.nl) for cell phones returning the associated cell provider
  • can be categorized by a more coarse lookup, like continent, country, region, town or number block owner, stored in a local array (~500 entries)

The setting for mod_cidlookup is:

<param name="url" value="http://webserviceprovider/lookup.php?number=${caller_id_number}"/>

lookup.php

This script returns some information about the caller, preferrably the name. It uses a local MySQL database and fetches info from some sites.

<?php
/*
 * Copyright (c) 2012, ACKspace foundation
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met: 
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * The views and conclusions contained in the software and documentation are those
 * of the authors and should not be interpreted as representing official policies, 
 * either expressed or implied, of the FreeBSD Project.
 */

define( "COUNTRY", "31" );
define( "REGION", "45" );
define( "PLUS", "00" );
define( "MYSQL_DB", "freeswitch_cidlookup" );

// Number dial plan, ~500 entries, shortened for wiki
$arrNumbers[1]['info'] = "(continent) Verenigde Staten";
$arrNumbers[1][4][4][1]['info'] = "Bermuda";
$arrNumbers[3]['info'] = "(continent) Europa";
$arrNumbers[3][1]['info'] = "(land) Nederland";
$arrNumbers[3][1][1][4]['info'] = "Testnetnummer van KPN Telecom";
$arrNumbers[3][1][4]['info'] = "(provincies) Oostelijk Noord-Brabant, Limburg";
$arrNumbers[3][1][4][5]['info'] = "(regio) Heerlen";
$arrNumbers[3][1][6]['info'] = "Mobiele nummers en Semafoondiensten";
$arrNumbers[3][1][6][1]['info'] = "Mobiele telefoon";
$arrNumbers[3][1][6][1][0]['info'] = "(GSM) KPN";
$arrNumbers[3][1][8][5]['info'] = "(type) Plaatsonafhankelijk/VoIP";
$arrNumbers[3][1][8][5][8][7]['info'] = "(VoIP) XS4ALL";

// Normalize the number
$arrNumberInfo = normalizeNumber( getVar( "number", true ) );

// Extension? try and get from dialplan
if ( $arrNumberInfo['type'] == "extension" )
{
    echo getExtension( $arrNumberInfo['local'] );
    exit;
}

if ( !function_exists( "mysql_connect" ))
{
    echo "ERROR";
    exit;
}

// Fetch the number from the database
if ( $strName = dbLookup( $arrNumberInfo ))
{
    echo $strName;
    exit;
}

// Nothing in the database?
// national number starting with 0[1-578]?
// Fetch number from website (check last get timestamp to prevent DoS
// put result in DB
if ( preg_match( "/^0[1-578].*/", $arrNumberInfo['national'] ) && $strName = fetchWebsiteResult( $arrNumberInfo['national'] ))
{
    echo $strName;

    // Add the name to the DB
    $arrNumberInfo['name'] = $strName;
    dbInsert( $arrNumberInfo );
    exit;
}
else if ( preg_match( "/^0[6].*/", $arrNumberInfo['national'] ) && $strName = fetchOptaResult( $arrNumberInfo['national'] ))
{
    // Number porting
    echo $strName;

    // Add the name to the DB, so we don't have to look it up anymore
    $arrNumberInfo['name'] = $strName;
    dbInsert( $arrNumberInfo );
    exit;
}

// Nothing on the website? Try and find the number in the array
if ( isset( $arrNumberInfo['international'] ))
{
    $strName = _getInfo( str_split( $arrNumberInfo['international'] ));
    echo $strName;
    exit;
}

/////////////////////////////////////////////////////////////////////
function fetchWebsiteResult( $_strNumber )
{

    if ( $strName = fetch_delefoondetective_nl( $_strNumber ))
        return $strName;

    if ( $strName = fetch_gevonden_cc( $_strNumber ))
        return $strName;

    if ( $strName = fetch_zoekenbel_nl( $_strNumber ))
        return $strName;

    if ( $strName = fetch_nummerzoeker_com( $_strNumber ))
        return $strName;

    if ( $strName = fetch_nummerid_com( $_strNumber ))
        return $strName;

    if ( $strName = fetch_gebeld_nl( $_strNumber ))
        return $strName;

    return false;
}

function fetchOptaResult( $_strNumber )
{
    $strPage = file_get_contents( "http://www.opta.nl/nl/nummers/nummers-zoeken/resultaat/?query=".$_strNumber."&page=1&portering=1" );

    if ( !preg_match( "/<strong>Huidige aanbieder<\/strong>.*?<p>(.*?)<\/p>/si", $strPage, $matches ))
        return false;

    return "(GSM) ".$matches[1];
}

function fetch_delefoondetective_nl( $_strNumber )
{
    $strPage = file_get_contents( "http://www.telefoondetective.nl/telefoonnummer/".$_strNumber."/" );

    if ( !preg_match( "/<div\sid=\"name\"><h\d>(.*?)<\/h\d><\/div>/i", $strPage, $matches ))
        return false;

    return $matches[1];
}

function fetch_gevonden_cc( $_strNumber )
{
    return false;
}

function fetch_zoekenbel_nl( $_strNumber )
{
    return false;
}

function fetch_nummerzoeker_com( $_strNumber )
{
    return false;
}


function fetch_nummerid_com( $_strNumber )
{
    return false;
}

function fetch_gebeld_nl( $_strNumber )
{
    return false;
}

function dbLookup( $_arrNumberInfo )
{
    if (!$db = mysql_connect( NULL, "username", "password" ))
        return false;

    // TODO: close db
    if (!mysql_select_db( MYSQL_DB, $db ))
        return false;

    // Prevent SQL injection on variables
    $country = mysql_real_escape_string( $_arrNumberInfo['country'] );
    $international = mysql_real_escape_string( $_arrNumberInfo['international'] );

    // Full number partial listing (experimental)
    $query = "SELECT name FROM telephone_names WHERE country_code=".$country." AND INSTR( '".$international."', number ) = 1 ORDER BY LENGTH(number), sortorder LIMIT 1";

    if (!$result = mysql_query( $query, $db ))
        return false;

    $row = mysql_fetch_row( $result );
    mysql_close( $db );
    return $row[0];
}


function dbInsert( $_arrNumberInfo )
{
    if (!$db = mysql_connect( NULL, "username", "password" ))
        return false;

    // TODO: close db
    if (!mysql_select_db( "freeswitch_cidlookup", $db ))
        return false;

    // Prevent SQL injection on variables
    $country = mysql_real_escape_string( $_arrNumberInfo['country'] );
    $international = mysql_real_escape_string( $_arrNumberInfo['international'] );
    $name = mysql_real_escape_string( $_arrNumberInfo['name'] );

    $query = "INSERT INTO telephone_names (country_code,number,name) VALUES (".$country.",'".$international."','".$name."')";

    if (!$result = mysql_query( $query, $db ))
    {
        echo mysql_error( $db );
        return false;
    }

    mysql_close( $db );

    return true;
}


function getExtension( $_strExtension )
{
    return false;
    //return "Ext. ".$_strExtension;
}

function normalizeNumber( $_strNumber )
{
    $arrInfo = array();

    if ( preg_match( "/^([19]\d+)/", $_strNumber, $matches ))
    {
        $arrInfo['local'] = $matches[1];
        $arrInfo['type'] = 'extension';
        return $arrInfo;
    }

    $_strNumber = preg_replace( "/^([2345678])/", COUNTRY.REGION.'$1', $_strNumber );

    // Add country on national dials
    $_strNumber = preg_replace( "/^(0)([^0].*)/", COUNTRY.'$2', $_strNumber );

    // Replace + and 00 international symbols before parsing (include space for URI conversion
    $_strNumber = preg_replace( "/^( |\+|00)/", '', $_strNumber );

    $arrInfo['country'] = intval( substr( $_strNumber, 0, 2 ));
    $arrInfo['international'] = $_strNumber;
    $arrInfo['type'] = 'international';

    // National number?
    if ( $arrInfo['country'] == intval( COUNTRY ))
    {
        // only works with countries of 2 digits; replaces it with a 0
        $arrInfo['national'] = "0".substr( $_strNumber, 2 );
        $arrInfo['type'] = 'national';
    }

    return $arrInfo;
};

function GetInfo( $_strNumber )
{
    global $arrNumbers;

    $_strNumber = preg_replace( "/^([2345678])/", COUNTRY.REGION.'$1', $_strNumber );

    // Add country on national dials
    $_strNumber = preg_replace( "/^(0)([^0].*)/", COUNTRY.'$2', $_strNumber );

    // Replace + and 00 international symbols before parsing
    $_strNumber = preg_replace( "/^(\+|00)/", '', $_strNumber );

    return _getInfo( str_split( $_strNumber ));
};

function _getInfo( $_arrNumber )
{
    global $arrNumbers;

    $arrInfo = array();

    $arrNumberInfo = $arrNumbers;
    $nIndent = 0;
    $strDigits = "";
    $strDetailedInfo = "";
    foreach ( $_arrNumber as $digit )
    {
        if ( isset( $arrNumberInfo[$digit] ) )
        {
            $strDigits .= $digit;
            $arrNumberInfo = $arrNumberInfo[$digit];
            if ( isset( $arrNumberInfo['info'] ) )
            {
                $arrInfo[] = str_repeat( "-", $nIndent ) . $strDigits . " " . $arrNumberInfo['info'];
                $strDigits = "";
                $strDetailedInfo = $arrNumberInfo['info'];
            }
            $nIndent++;
        } else {
            break;
        }
    }

    return $strDetailedInfo;
}

////////////////////////////////////////////////////////////////////////////////
// Helpers
////////////////////////////////////////////////////////////////////////////////
function getVar( $_strVarName, $_bAllowGet = false )
{
    // If _POST var is set, return _POST var,
    // else, if _GET var is set and is allowed, return _GET var
    // else, requested var not found: return NULL
    if ( isset( $_POST[ $_strVarName ] ) )
        return $_POST[ $_strVarName ];
    else if ( isset( $_GET[ $_strVarName ] ) && ($_bAllowGet == true) )
        return $_GET[ $_strVarName ];
    else
        return NULL;
}
?>

dialing out

This default dialplan snippet does a caller id lookup, and updates the callee name which will be visible on the local extension

<extension name="National_numbers">
    <condition field="destination_number" expression="^0([1-578]\d{8})$">
        <action application="set" data="effective_caller_id_number=${outbound_caller_id}"/>
        <action application="export" data="callee_id_name=${cidlookup(0031$1)}" />
        <action application="bridge" data="sofia/gateway/myLandLineProvider/31$1"/>
    </condition>
</extension>

incoming calls

This public dialplan snippet somewhat at the top sets the number (if any) first, checks if it has an international prefix, does a lookup for incoming calls and will set the name accordingly.

The second part will strip any leading + sign

<extension name="fix_cidnam" continue="true">
    <!--make sure the module is loaded, or else loading it will kill our call!-->
    <!-- Simple case: name=number or name is empty -->
    <!-- and number is a 10digit (excluding optional leading 1), in nanpa nxx-nxx-xxxx form -->
    <!-- will skipurl lookup if not a 10digit # (don't lookup INTL), and instead just query the SQL -->
    <condition field="${module_exists(mod_cidlookup)}" expression="true"/>
    <condition field="caller_id_name" expression="^${caller_id_number}$|^$"/>
    <condition field="caller_id_number" expression="^(\+|00)(\d+)$">
        <action application="cidlookup" data="00$2"/>
        <anti-action application="cidlookup" data="${caller_id_number}"/>
    </condition>
</extension>

<extension name="fix_cidnam_plus" continue="true">
    <!-- if the name starts with + followed by digits, strip the
         + and then pass the number -->
    <condition field="caller_id_name" expression="^\+(1[2-9]\d\d[2-9]\d{6})$">
        <action application="cidlookup" data="$1"/>
    </condition>
</extension>

todo

items stored in the database will not be updated anymore. The only way to refresh the number's information is to remove the entry manually which will cause a new lookup the next time that number is requested.

Simple intercom

dialplan snippet

Upon dialing *<number>, this snippet will create a conference for the caller, and will try for 5 seconds to add the callee as <number>_intercom which is an auto-answer line 2 setup on the cisco phones containing a sip image.

<extension name="extension-intercom">
    <condition field="destination_number" expression="^\*(1[09]\d\d)$" break="on-false">
        <action application="set" data="dialed_extension=$1"/>
        <action application="sleep" data="300"/>
    </condition>

    <condition>
        <action application="set" data="api_hangup_hook=conference 412 kick all"/>
        <action application="answer"/>
        <action application="export" data="sip_invite_params=intercom=true"/>
        <action application="export" data="sip_auto_answer=true"/>
        <action application="set" data="conference_auto_outcall_caller_id_name=$${effective_caller_id_name}"/>
        <action application="set" data="conference_auto_outcall_caller_id_number=$${effective_caller_id_number}"/>
        <action application="set" data="conference_auto_outcall_timeout=5"/>
        <action application="set" data="conference_auto_outcall_flags=mute"/>
        <action application="conference_set_auto_outcall" data="user/${dialed_extension}_intercom@${domain_name}"/>
        <action application="conference" data="412@intercom"/>

        <!-- Shouldn't be needed -->
        <action application="conference" data="412 kick all"/>
    </condition>
</extension>

todo

There is a auto_answer perl script that logs into the phone and changes the auto-answer setting: I might want to upload it here.

Space state

This script sets the spacestate variable according to what the webservice API returns. It then can be used in the dialplan to, for example, play a sound file or do call forwarding. Currently it is used to build a sound phrase for playing it as a greeting within our IVR

spacestate.js

// Checks the space state API: if "open" is not 'true',
// it's assumed closed and plays a sound file

if (session.ready())
{
    session.preAnswer();

    var state = fetchUrl( "https://ackspace.nl/status.php" );
    if (state == false)
        console_log( 3, "could not fetch space state");
    else
    {
        // Remove newlines
        state = state.replace( /[\r\n]/g,"");

        if( !state.match(  /.*"open"[ \t]*:[ \t]*true.*/i ) )
        {
            console_log(4, "Space seems to be closed, or could not parse result.\n");
            session.setVariable("spacestate", "closed");
        } else {
            console_log(4, "Space is open!\n");
            session.setVariable("spacestate", "open");
        }
    }
}

dialplan and ivr

The javascript is executed just before executing the IVR

<action application="javascript" data="spacestate.js"/>
<action application="ivr" data="ackspace_ivr"/>

and the corresponding IVR setting is:

greet-long="phrase:ackspace_welcome:${spacestate}"


phrase

This snippet is put under lang/nl/IVR/ackspace.xml and checks the given variable whether it is set to 'open'

<macro name="ackspace_welcome">
    <input pattern="^(open)$">
        <match>
            <action function="play-file" data="ivr/welkom.wav"/>
            <action function="sleep" data="300"/>
            <action function="play-file" data="phrase:ackspace_welcome_short"/>
        </match>
        <nomatch>
            <action function="play-file" data="ivr/welkom.wav"/>
            <action function="sleep" data="300"/>
            <action function="play-file" data="ivr/gesloten.wav"/>
            <action function="sleep" data="2000"/>
            <action function="play-file" data="phrase:ackspace_welcome_short"/>
        </nomatch>
    </input>
</macro>

closing announcement

While this script is not in use anymore, since we're a 24/7 space now, it still is fun to trigger announcements using cron jobs

it used to trigger on 9:30PM on weekdays, and 4:30PM on saturday. At first I tried to retrigger the events in the dialplan for the next occurrence, but cron jobs are way more reliable (and easier)

crontab for freeswitch user

30  21  *   *   1-5 /usr/local/freeswitch/bin/fs_cli -x "originate loopback/1399 closure"
30  16  *   *   6   /usr/local/freeswitch/bin/fs_cli -x "originate loopback/1399 closure"

paging.js

This script takes the dialstring of all members withing call group 'intercom' and puts them as conference-auto-out-call separately. After that, the conference is started with the sound extension, and all parsed extensions will be called automatically (assuming that extension has auto-answer for that line) When the sound extension is done and hangs up, everyone is kicked out of the conference.

if (session.ready())
{
    session.answer();

    console_log(4, "building-wide paging\n");

    session.execute("set","pageGroup=${group_call(intercom@${domain_name}+E)}\n");
    var pageGroup = session.getVariable("pageGroup");
    var arrPageGroup = pageGroup.split( ":_:" );
    for ( var idx = 0; idx < arrPageGroup.length; idx++ )
    {
        var user = arrPageGroup[ idx ];
        user = user.replace( /\n/g, "" );
        user = user.replace( /\[[^\]]*\](.*)/g, "$1" );
        session.execute("conference_set_auto_outcall", user);
    }
}

dialplan

The announcement consists of two parts: triggering a group-intercom and executing a 'closure' extension which played the appropriate sounds

<extension name="group_page">
    <condition field="destination_number" expression="^(1399)$">
        <action application="set" data="api_hangup_hook=conference 412 kick all"/>
        <action application="answer"/>
        <action application="export" data="sip_invite_params=intercom=true"/>
        <action application="export" data="sip_auto_answer=true"/>
        <action application="set" data="conference_auto_outcall_caller_id_name=$${effective_caller_id_name}"/>
        <action application="set" data="conference_auto_outcall_caller_id_number=$${effective_caller_id_number}"/>
        <action application="set" data="conference_auto_outcall_timeout=5"/>
        <action application="set" data="conference_auto_outcall_flags=mute"/>
        <action application="javascript" data="paging.js"/>
        <action application="set" data="res=${sched_api +1 none conference 412 play tone_stream://path=${base_dir}/conf/beep.ttml}"/>
        <action application="conference" data="412@intercom"/>
        <action application="conference" data="412 kick all"/>
    </condition>
</extension>

<extension name="closing announcement">
    <condition field="destination_number" expression="^closure|1398$" break="on-false">
        <action application="set" data="language=nl"/>
        <action application="answer"/>
        <action application="sleep" data="2000"/>
        <action application="set_audio_level" data="write -2"/>
        <action application="playback" data="../../../stationsbel.mp3"/>
        <action application="set_audio_level" data="write 0"/>
        <action application="playback" data="../../../nl/nl/xander/ivr/sluiten.wav"/>
        <action application="sleep" data="1000"/>
        <action application="set_audio_level" data="write -2"/>
        <action application="playback" data="../../../game_over.mp3"/>
    </condition>
</extension>