700 lines
18 KiB
PHP
700 lines
18 KiB
PHP
<?
|
|
|
|
/* libyateivr.php
|
|
* This file is part of the YATE Project http://YATE.null.ro
|
|
*
|
|
* Object oriented IVR
|
|
*
|
|
* Yet Another Telephony Engine - a fully featured software PBX and IVR
|
|
* Copyright (C) 2004-2006 Null Team
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
/*
|
|
WARNING: This file requires PHP 5
|
|
*/
|
|
|
|
|
|
require_once("libyate.php");
|
|
|
|
/**
|
|
* The IVR class encapsulates an instance of IVR
|
|
*/
|
|
class IVR
|
|
{
|
|
/** String: Name of this IVR */
|
|
var $ivrname;
|
|
|
|
/** Array: State operation table */
|
|
var $optable;
|
|
|
|
/** String: Name of the current state */
|
|
var $curstate;
|
|
|
|
/** Array: Name of files to play in sequence */
|
|
var $playfile;
|
|
|
|
/**
|
|
* Base class constructor
|
|
*/
|
|
function IVR()
|
|
{
|
|
$this->ivrname = null;
|
|
$this->optable = array();
|
|
$this->curstate = "";
|
|
$this->playfile = array();
|
|
}
|
|
|
|
/**
|
|
* Helper method to output a string prefixed by IVR name
|
|
* @param $text Text to put in logs or on console
|
|
*/
|
|
function Output($text)
|
|
{
|
|
if ($text{0} != ":")
|
|
$text = ": $text";
|
|
Yate::Output($this->GetName() . $text);
|
|
}
|
|
|
|
/**
|
|
* Helper method to output a debug string prefixed by IVR name
|
|
* @param $text Text to put in logs or on console
|
|
*/
|
|
function Debug($text)
|
|
{
|
|
if ($text{0} != ":")
|
|
$text = ": $text";
|
|
Yate::Debug($this->GetName() . $text);
|
|
}
|
|
|
|
/**
|
|
* Get the name of the IVR
|
|
*/
|
|
function GetName()
|
|
{
|
|
return $this->ivrname;
|
|
}
|
|
|
|
/**
|
|
* Get the current state of the IVR
|
|
*/
|
|
function GetState()
|
|
{
|
|
return $this->curstate;
|
|
}
|
|
|
|
/**
|
|
* Change the state of this IVR instance
|
|
* @param $state New state of the IVR
|
|
*/
|
|
function SetState($state)
|
|
{
|
|
$this->Debug("::SetState('$state')");
|
|
$this->curstate = $state;
|
|
}
|
|
|
|
/**
|
|
* Try and if found execute a table described operation - internal use
|
|
* @param $state State to match
|
|
* @param $text Event name of DTMF
|
|
* @return True if handled
|
|
*/
|
|
private function TryTable($state,$text)
|
|
{
|
|
if (isset($this->optable["$state:$text"])) {
|
|
$op = explode(":",$this->optable["$state:$text"]);
|
|
$this->Debug("Found table operation '" . $op[0] . "'");
|
|
switch ($op[0]) {
|
|
case "output": // Output a text to console or log file
|
|
$this->Output($op[1]);
|
|
break;
|
|
case "debug": // Output a text if debugging is enabled
|
|
$this->Debug($op[1]);
|
|
break;
|
|
case "state": // Change local IVR state
|
|
$this->SetState($op[1]);
|
|
break;
|
|
case "play": // Play one or many sound files
|
|
array_shift($op);
|
|
$this->PlayFile($op);
|
|
break;
|
|
case "tone": // Start playing a tone
|
|
$this->PlayTone($op[1]);
|
|
break;
|
|
case "stop": // Stop sound playback
|
|
$this->PlayStop();
|
|
break;
|
|
case "jump": // Jump to another IVR, leave this one
|
|
IVR::Jump($op[1],(isset($op[2]) ? $op[2] : null));
|
|
break;
|
|
case "call": // Call into another IVR, put this on stack
|
|
IVR::Call($op[1],(isset($op[2]) ? $op[2] : null));
|
|
break;
|
|
case "leave": // Leave this IVR, return to one on stack
|
|
IVR::Leave(isset($op[1]) ? $op[1] : null);
|
|
break;
|
|
case "progress": // Emit a call progress notification
|
|
case "ringing": // Emit a call ringing notification
|
|
case "answered": // Emit an answer notification
|
|
$m = new Yate("call." . $op[0]);
|
|
$m->id = "";
|
|
$m->SetParam("id",IVR::ChannelID());
|
|
$m->SetParam("targetid",IVR::TargetID());
|
|
$m->Dispatch();
|
|
break;
|
|
case "hangup": // Hangup the entire IVR
|
|
IVR::Hangup();
|
|
break;
|
|
default:
|
|
$this->Output("Invalid table operation '" . $op[0] . "'");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Perform table described operations
|
|
* @return True if operation was executed from table
|
|
*/
|
|
function OperTable($text)
|
|
{
|
|
if ($this->TryTable($this->curstate,$text) || $this->TryTable("",$text))
|
|
return true;
|
|
if (strlen($text) != 1)
|
|
return false;
|
|
return $this->TryTable($this->curstate,".") || $this->TryTable("",".");
|
|
}
|
|
|
|
/**
|
|
* Method called when this IVR is entered by jump or call
|
|
* @param $state Initial state requested by the caller IVR
|
|
*/
|
|
function OnEnter($state)
|
|
{
|
|
$this->Debug("::OnEnter('$state')");
|
|
$this->OperTable("enter");
|
|
}
|
|
|
|
/**
|
|
* Method called when leaving this IVR by jump or return
|
|
*/
|
|
function OnLeave()
|
|
{
|
|
$this->Debug("::OnLeave()");
|
|
}
|
|
|
|
/**
|
|
* Method called when returning to this IVR from a called one
|
|
* @param $retval Value returned by the called IVR
|
|
*/
|
|
function OnReturn($retval)
|
|
{
|
|
$this->Debug("::OnReturn('$retval')");
|
|
}
|
|
|
|
/**
|
|
* Method called when this IVR receives an event
|
|
* @param $event Reference to received event
|
|
* @return True if the event was handled, false if undesired
|
|
*/
|
|
function OnEvent(&$event)
|
|
{
|
|
$this->Debug("::OnEvent(" . $event->type . " '" . $event->name . "')");
|
|
switch ($event->type) {
|
|
case "incoming":
|
|
return $this->OnMessage($event->name,$event);
|
|
case "answer":
|
|
return $this->OnDispatch($event->name,$event);
|
|
case "setlocal":
|
|
return $this->OnSetting($event->name,$event->retval,$event->handled);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Method called on stacked IVRs when the current one didn't process an event
|
|
* @param $event Reference to received event
|
|
* @return True if the event was handled, false if undesired
|
|
*/
|
|
function OnUnhandled(&$event)
|
|
{
|
|
$this->Debug("::OnUnhandled(" . $event->type . " '" . $event->name . "')");
|
|
if ($event->type != "incoming")
|
|
return false;
|
|
if ($event->name != "call.execute" && $event->name != "chan.disconnected")
|
|
return false;
|
|
return $this->OnMessage($event->name,$event);
|
|
}
|
|
|
|
/**
|
|
* Method called when a message from Yate arrives at this IVR
|
|
* @param $name Name of the message
|
|
* @param $event Reference to incoming message event
|
|
* @return True if the event was handled, false if undesired
|
|
*/
|
|
function OnMessage($name, &$event)
|
|
{
|
|
$this->Debug("::OnMessage('$name')");
|
|
switch ($name) {
|
|
case "call.execute":
|
|
return $this->OnExecute($event);
|
|
case "chan.dtmf":
|
|
return $this->OnDTMF($event->GetValue("text"));
|
|
case "chan.notify":
|
|
$notify = $event->GetValue("event");
|
|
if (($notify !== null) && ($notify != "wave"))
|
|
return $this->OnNotify($notify,$event);
|
|
if ($event->GetValue("reason") == "replaced")
|
|
return true;
|
|
return $this->PlayNext() || $this->OnEOF();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Method called when a message generated by this IVR has finished dispatching
|
|
* @param $name Name of the message
|
|
* @param $event Reference to message answered event
|
|
* @return True if the event was handled, false if undesired
|
|
*/
|
|
function OnDispatch($name, &$event)
|
|
{
|
|
$this->Debug("::OnDispatch('$name')");
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Method called after getting or setting a module or engine parameter
|
|
* @param $name Name of the parameter
|
|
* @param $value Current value of the parameter
|
|
* @param $ok True if th operation was successfull, false if invalid
|
|
*/
|
|
function OnSetting($name, $value, $ok)
|
|
{
|
|
$this->Debug("::OnSetting('$name','$value'," . Yate::Bool2str($ok) . ")");
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Method called on the call.execute event. This is the first event
|
|
* received and only one IVR will get a chance to handle it
|
|
* @param $event Reference to received event
|
|
* @return True if the event was handled, false if undesired
|
|
*/
|
|
function OnExecute(&$event)
|
|
{
|
|
$this->Debug("::OnExecute()");
|
|
if ($this->OperTable("execute")) {
|
|
$event->handled = true;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Method called when a file finished playing, by default it starts playing
|
|
* the next in queue
|
|
*/
|
|
function OnEOF()
|
|
{
|
|
$this->Debug("::OnEOF()");
|
|
$this->OperTable("eof");
|
|
}
|
|
|
|
/**
|
|
* Method called on named notifications
|
|
* @param $name Name of the notification (parameter "event" in message)
|
|
* @param $event Reference to notification message event
|
|
* @return True if the notification was handled, false if undesired
|
|
*/
|
|
function OnNotify($name, &$event)
|
|
{
|
|
if ($name == "dtmf")
|
|
return $this->OnDTMF($event->GetValue("text"));
|
|
$this->Debug("::OnNotify('$name')");
|
|
return $this->OperTable($name);
|
|
}
|
|
|
|
/**
|
|
* Method called for each DTMF received for this IVR
|
|
* @param $tone Received key press, 0-9, A-D or F(lash)
|
|
* @return True if the key was handled, false if undesired
|
|
*/
|
|
function OnDTMF($key)
|
|
{
|
|
$this->Debug("::OnDTMF('$key')");
|
|
return $this->OperTable($key);
|
|
}
|
|
|
|
|
|
/**
|
|
* Play a wave file or add it to the queue
|
|
* @param $file1,$file2,... Path to files to play
|
|
* @param $clear Optional - true to clear the queue and start playing now
|
|
*/
|
|
function PlayFile()
|
|
{
|
|
$args = func_get_args();
|
|
$n = count($args);
|
|
if ($n == 1 && is_array($args[0])) {
|
|
$args = $args[0];
|
|
$n = count($args);
|
|
}
|
|
if ($n < 1)
|
|
return;
|
|
$clear = false;
|
|
$last = $args[$n - 1];
|
|
if ($last === false)
|
|
$n--;
|
|
if (($last === true) || ($last == "clear")) {
|
|
$n--;
|
|
$clear = true;
|
|
}
|
|
if ($n == 1 && is_array($args[0])) {
|
|
$args = $args[0];
|
|
$n = count($args);
|
|
}
|
|
if ($clear)
|
|
$this->playfile = array();
|
|
for ($i = 0; $i < $n; $i++)
|
|
$this->playfile[] = $args[$i];
|
|
if ($clear)
|
|
$this->PlayNext();
|
|
}
|
|
|
|
/**
|
|
* Start playing a tone, clear the file queue
|
|
* @param $tone Name of the tone to play
|
|
*/
|
|
function PlayTone($tone)
|
|
{
|
|
$this->playfile = array();
|
|
$m = new Yate("chan.attach");
|
|
$m->id = "";
|
|
$m->SetParam("source","tone/$tone");
|
|
$m->SetParam("single",true);
|
|
$m->Dispatch();
|
|
}
|
|
|
|
/**
|
|
* Play next file from the play queue
|
|
* @return True if play started, false if queue was empty
|
|
*/
|
|
function PlayNext()
|
|
{
|
|
$file = array_shift($this->playfile);
|
|
if ($file === null)
|
|
return false;
|
|
$m = new Yate("chan.attach");
|
|
$m->id = "";
|
|
$m->SetParam("notify",IVR::ChannelID());
|
|
$m->SetParam("source","wave/play/$file");
|
|
$m->SetParam("single",true);
|
|
$m->Dispatch();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Stop the file playback and clear the play queue
|
|
*/
|
|
function PlayStop()
|
|
{
|
|
$this->playfile = array();
|
|
$m = new Yate("chan.attach");
|
|
$m->id = "";
|
|
$m->SetParam("source","wave/play/-");
|
|
$m->SetParam("single",true);
|
|
$m->Dispatch();
|
|
}
|
|
|
|
/**
|
|
* Retrive the IVR's channel ID, initialize it if required
|
|
* @return ID of the IVR call leg
|
|
*/
|
|
static function ChannelID()
|
|
{
|
|
global $yate_ivr_channel;
|
|
if (!isset($yate_ivr_channel))
|
|
$yate_ivr_channel = "ivr/" . uniqid(rand(),1);
|
|
return $yate_ivr_channel;
|
|
}
|
|
|
|
/**
|
|
* Set the IVR's channel ID, must be called early
|
|
* @param $id Desired ID of the IVR call leg
|
|
* @return True if the new ID was set successfully, false if already set
|
|
*/
|
|
static function SetChannelID($id)
|
|
{
|
|
global $yate_ivr_channel;
|
|
if (($id != "") && !isset($yate_ivr_channel)) {
|
|
$yate_ivr_channel = $id;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Retrive the target channel ID
|
|
* @return ID of the call leg that called into the IVR
|
|
*/
|
|
static function TargetID()
|
|
{
|
|
global $yate_ivr_target;
|
|
return $yate_ivr_target;
|
|
}
|
|
|
|
/**
|
|
* Jump to another IVR, leave the current one
|
|
* @param $ivrname Name of the IVR to jump to
|
|
* @param $state Desired initial state of the new IVR, null to use default
|
|
* @return True if jumped to a new IVR, false if it doesn't exist
|
|
*/
|
|
static function Jump($ivrname, $state = null)
|
|
{
|
|
global $yate_ivr_current;
|
|
|
|
$obj = IVR::CreateIVR($ivrname);
|
|
if ($obj === null)
|
|
return false;
|
|
|
|
if ($yate_ivr_current !== null)
|
|
$yate_ivr_current->OnLeave();
|
|
$yate_ivr_current = $obj;
|
|
$yate_ivr_current->OnEnter($state);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Call another IVR, current one is placed on the stack
|
|
* @param $ivrname Name of the IVR to call to
|
|
* @param $state Desired initial state of the new IVR
|
|
* @return True if called to a new IVR, false if it doesn't exist
|
|
*/
|
|
static function Call($ivrname, $state = null)
|
|
{
|
|
global $yate_ivr_current;
|
|
global $yate_ivr_stack;
|
|
|
|
$obj = IVR::CreateIVR($ivrname);
|
|
if ($obj === null)
|
|
return false;
|
|
|
|
if ($yate_ivr_current !== null)
|
|
array_unshift($yate_ivr_stack, $yate_ivr_current);
|
|
$yate_ivr_current = $obj;
|
|
$yate_ivr_current->OnEnter($state);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Leave this IVR and return to the caller on the stack.
|
|
* If the stack is empty hang up the channel
|
|
* @param $retval Value to return to the caller
|
|
* @return True if returned OK, false if stack was empty and we're hanging up
|
|
*/
|
|
static function Leave($retval = null)
|
|
{
|
|
global $yate_ivr_current;
|
|
global $yate_ivr_stack;
|
|
|
|
if ($yate_ivr_current === null)
|
|
return false;
|
|
$yate_ivr_current->OnLeave();
|
|
$yate_ivr_current = array_shift($yate_ivr_stack);
|
|
if ($yate_ivr_current === null)
|
|
return false;
|
|
$yate_ivr_current->OnReturn($retval);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Change the state of the current IVR.
|
|
* @param $state New state of the IVR
|
|
*/
|
|
static function State($state)
|
|
{
|
|
global $yate_ivr_current;
|
|
|
|
if ($yate_ivr_current !== null)
|
|
$yate_ivr_current->SetState($state);
|
|
}
|
|
|
|
/**
|
|
* Register an IVR by its class name
|
|
* @param $ivrname Name of the IVR to register
|
|
* @param $classname Name of the class to instantiate, defaults to name of IVR
|
|
* @return True if registered, false if invalid class or IVR already registered
|
|
*/
|
|
static function Register($ivrname, $classname = null)
|
|
{
|
|
global $yate_ivr_register;
|
|
|
|
IVR::InitIVR();
|
|
if (isset($yate_ivr_register[$ivrname])) {
|
|
Yate::Output("IVR: Already registered IVR '$ivrname'");
|
|
return false;
|
|
}
|
|
if ($classname === null)
|
|
$classname = $ivrname;
|
|
if (!class_exists($classname)) {
|
|
Yate::Output("IVR: Inexistent class '$classname' for IVR '$ivrname'");
|
|
return false;
|
|
}
|
|
$yate_ivr_register[$ivrname] = $classname;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get the name of the current IVR
|
|
* @return Name of the IVR, null if none running
|
|
*/
|
|
static function Current()
|
|
{
|
|
global $yate_ivr_current;
|
|
|
|
if (isset($yate_ivr_current) && ($yate_ivr_current !== null))
|
|
return $yate_ivr_current->GetName();
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Hang up from the IVR side
|
|
*/
|
|
static function Hangup()
|
|
{
|
|
global $yate_ivr_current;
|
|
|
|
IVR::CleanupIVR();
|
|
$yate_ivr_current = null;
|
|
}
|
|
|
|
/**
|
|
* Run the IVR system
|
|
* @param $ivrname Name of the initial IVR to start
|
|
*/
|
|
static function Run($ivrname)
|
|
{
|
|
global $yate_ivr_target;
|
|
global $yate_ivr_current;
|
|
|
|
if (IVR::Jump($ivrname)) {
|
|
$init_id = true;
|
|
while ($yate_ivr_current !== null) {
|
|
$ev = Yate::GetEvent();
|
|
if ($ev === true)
|
|
continue;
|
|
if ($ev === false)
|
|
break;
|
|
if (($ev->type == "incoming") && ($ev->name == "call.execute"))
|
|
$yate_ivr_target = $ev->GetValue("id");
|
|
IVR::EventIVR($ev);
|
|
if ($init_id && ($yate_ivr_current !== null)) {
|
|
$init_id = false;
|
|
Yate::SetLocal("id",IVR::ChannelID());
|
|
Yate::Install("chan.dtmf",100,"targetid",IVR::ChannelID());
|
|
Yate::Install("chan.notify",100,"targetid",IVR::ChannelID());
|
|
}
|
|
if ($ev && ($ev->type == "incoming")) {
|
|
if ($ev->handled && $ev->name == "call.execute")
|
|
$ev->params["targetid"] = IVR::ChannelID();
|
|
$ev->Acknowledge();
|
|
}
|
|
}
|
|
IVR::CleanupIVR();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create an IVR instance by name, used internally only
|
|
* @param $ivrname Name of the IVR to create
|
|
* @return Newly created IVR object
|
|
*/
|
|
private static function CreateIVR($ivrname)
|
|
{
|
|
global $yate_ivr_register;
|
|
|
|
IVR::InitIVR();
|
|
if (isset($yate_ivr_register[$ivrname])) {
|
|
$obj = new $yate_ivr_register[$ivrname];
|
|
$obj->ivrname = $ivrname;
|
|
return $obj;
|
|
}
|
|
Yate::Output("IVR: Requested unknown IVR '$ivrname'");
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Cleanup the remaining IVR objects on stack, used internally only
|
|
*/
|
|
private static function CleanupIVR()
|
|
{
|
|
global $yate_ivr_current;
|
|
global $yate_ivr_stack;
|
|
|
|
do {
|
|
if ($yate_ivr_current !== null)
|
|
$yate_ivr_current->OnLeave();
|
|
$yate_ivr_current = array_shift($yate_ivr_stack);
|
|
} while ($yate_ivr_current !== null);
|
|
}
|
|
|
|
/**
|
|
* Run an event through the current IVR and the call stack
|
|
* @param $event Reference to event to run through the IVR stack
|
|
* @return True if the event was handled, false if not
|
|
*/
|
|
private static function EventIVR(&$event)
|
|
{
|
|
global $yate_ivr_current;
|
|
global $yate_ivr_stack;
|
|
|
|
if ($yate_ivr_current === null)
|
|
return false;
|
|
if ($yate_ivr_current->OnEvent($event))
|
|
return true;
|
|
foreach ($yate_ivr_stack as &$obj)
|
|
if ($obj->OnUnhandled($event))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Initialize the IVR system if not already initialized, used internally
|
|
*/
|
|
private static function InitIVR()
|
|
{
|
|
global $yate_ivr_target;
|
|
global $yate_ivr_register;
|
|
global $yate_ivr_current;
|
|
global $yate_ivr_stack;
|
|
global $yate_ivr_files;
|
|
if (isset($yate_ivr_register))
|
|
return;
|
|
Yate::Debug("IVR::InitIVR()");
|
|
$yate_ivr_target = null;
|
|
$yate_ivr_register = array();
|
|
$yate_ivr_current = null;
|
|
$yate_ivr_stack = array();
|
|
$yate_ivr_files = "";
|
|
}
|
|
}
|
|
|
|
/* vi: set ts=8 sw=4 sts=4 noet: */
|
|
?>
|