View Source

<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{zone-template-instance:ZFPROP:Proposal Zone Template}
{composition-setup}

{zone-data:component-name}
Zend_Telephony_Asterisk
{zone-data}

{zone-data:proposer-list}
[~freak]
{zone-data}

{zone-data:liaison}
A smart person from the CR-Team perhaps?
{zone-data}

{zone-data:revision}
0.1 - 29 December 2010: Initial Draft (wiki revision: 5)
0.1.1 - 26 February 2011: Fixed layout
{zone-data}

{zone-data:overview}
Zend_Asterisk is a component that provides means for easy communication with an Asterisk PBX.
{zone-data}

{zone-data:references}
* [Asterisk Home Page|http://www.asterisk.org/]
* [Digium, company behind Asterisk|http://www.digium.com/]
* [PHPagi|http://phpagi.sourceforge.net/]
* [Asterisk API docs|http://www.asterisk.org/astdocs/api/index.html]
* [AJAM|http://www.voip-info.org/wiki/view/Aynchronous+Javascript+Asterisk+Manager+(AJAM)]
* [Asterisk::AGI (Perl)|http://search.cpan.org/~jamesgol/asterisk-perl-0.09/lib/Asterisk/AGI.pm]
* [Asterisk-java|http://www.voip-info.org/wiki/view/Asterisk-java]
{zone-data}

{zone-data:requirements}
* This component *will* provide means to communicate with the Asterisk PBX using its AGI IPC protocol.
* This component *will* provide means to manage an instance of Asterisk PBX using its AMI protocol/socket.
* This component *may* provide an alternative or supplement to AJAM.
* This component *may* (at a later stage) provide means to communicate with Asterisk's channel-API, its application API, its codec translator API and its file format API.
* This component *may* correctly reads a developers mind for intent and generate the right configuration file.
* This component, and its documentation will assume basic knowledge of Telephony systems and Asterisk in specific.
* This component *will* support Asterisk 1.8.
* This component *may* or *may not* support other (previous) versions of the Asterisk software.
{zone-data}

{zone-data:dependencies}
* Zend\Exception
* Something that handles events
{zone-data}

{zone-data:operation}
AGI:
AGI is a protocol invented, and used by Asterisk to invoke an application separate from Asterisk, e.g. a ZF2 application. In the asterisk dialplan you can specify at what point the application needs to be invoked. At that specified point the application is called as an executable and various arguments about the current call are supplied via STDIN.
In the app (which is basically run in CLI mode), the Asterisk\Agi component is instantiated, and it will read the supplied arguments from asterisk, these are stored as properties in the component, and cannot be changed after instantiation. Meaning that the AGI component basically represents one phonecall/channel.
Once the component has been instantiated and the arguments been parsed, one can specify commands which are then executed. Each command returns a boolean (true on success, false on failure (makes sense, right?)), and if the asterisk server supplied additional data with the component it is stored as property in the component, from which it can be later retrieved, prior to performing the next command.

{zone-data}
{zone-data:milestones}
* Milestone 0: \[DONE\] Thought a bit about it
* Milestone 1: design notes will be published here.
* Milestone 2: Working prototype checked into the incubator.
* Milestone 3: Proposal is announced to both the Asterisk and ZF community.
* Milestone 4: Proposal is accepted
* Milestone 5: ZF rocks even more.
{zone-data}


{zone-data:class-list}
* Zend\Telephony\Asterisk\Agi
* Zend\Telephony\Asterisk\Ami
* Relevant exception classes
{zone-data}

{zone-data:use-cases}
{deck:id=UseCases1}
{card:label=Basic example}
#!/usr/bin/php
<?php

require_once __DIR__ . '/../application/Init.php';
use Zend\Telephony\Asterisk\Agi;

$options = array('debugFile' => '/debug');
$agi = new Agi($options);
$agi->answer();

// We can't just write to stdout, so we're logging to a file
$agi->writeDebug($agi::DEBUG_INFO, $agi->exec('PLAYBACK vm-INBOX'));
$agi->writeDebug($agi::DEBUG_INFO, print_r($agi->getLastResult(),1));

sleep(20);
$agi->hangup();{card}
{card:label=How Asterisk works}
For now I will only assume usage of the AGI interface. The story is that someone calls an asterisk server (#1). The caller will hear a message describing what options they can choose from (#6). Then this person wants to hear this message again (#9). When listening to it again (#11), he chooses option 3 (sales) (#13), but no one is present (#17). As such, the caller is redirected to a voicemail (#19). After having recorded the voicemail (#24) sales is sent an email that a voicemail is awaiting them (#24). PHP shuts down (#26).
{code}
Asterisk: PHP
1. Asterisk receives a phone call
2. Asterisk starts the php application
3. PHP is started up.
4. Asterisk sends all the details of the
conversation (caller id, protocols used,
number dialed, etc)
5. The PHP app reads (stdin) all the parameters from stdin
5b. PHP tells asterisk to answer the call
6. The PHP app tells (stdout) asterisk to play ~/welcome.wav
7. Asterisk begins playing the welcome.wav file
8. PHP tells (stdout) asterisk its awaiting user input for a certain
amount of time (the length/duration of the wav file)
9. Asterisk doesn't receive any digits from the caller
As such tells php no numbers were pressed
10. The PHP app tells asterisk to play ~/welcome.wav (again)
11. Asterisk begins playing the welcome.wav file
12. PHP tells Asterisk it's awaiting user input.
and begin to wait for input on STDIN
13. Asterisk receives the number 3.
13. Asterisk tells php that the number 3 was pressed
14. PHP receives the nummero 3 from asterisk (STDIN)
15. PHP tells asterisk to forward the call/channel to
the extension of sales
16. Asterisk begins dialling the extension
17. PHP doesn't get confirmation the extension is answered.
tells Asterisk to stop dialling the extension
18 Asterisk stops dialling the extension
confirms the stop to php's stdin.
19. PHP tells asterisk to play ~/voicemail.wav
20. Asterisk plays the given file
21. When the file is done playing, Asterisk
confirms that to php.
22. PHP receives over STDIN that the file was done playing.
23. PHP tells to receive any further input to voicemail-xxx.wav
24. The user hangs up, asterisk tells php about it
25. PHP emails the salesmanager that there's a voicemail awaiting for him.
26. PHP tells asterisk (STDOUT) to close the channel
27. Asterisk hangs up PHP quits {code}
{card}
{deck}

{zone-data}

{zone-data:skeletons}
Code is still a WIP, but can be found on github, here: https://github.com/Freeaqingme/zf2/tree/asterisk/library/Zend/Telephony

{deck:id=UseCases1}
{card:label=\Telephony\Asterisk}

{code}
<?php
/**
* Zend Framework
*
* LICENSE
*
* This source file is subject to the new BSD license that is bundled
* with this package in the file LICENSE.txt.
* It is also available through the world-wide-webat this URL:
* http://framework.zend.com/license/new-bsd
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@zend.com so we can send you a copy immediately.
*
* @category Zend
* @package Zend_Telephony_Asterisk
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
* @author Dolf Schimmel - Sponsored by HostDelight: www.HostDelight.com
*/

/**
* @namespace
*/
namespace Zend\Telephony\Asterisk;

/**
*
* @category Zend
* @package Zend_Telephony_Asterisk
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
* @author Dolf Schimmel - Sponsored by HostDelight: www.HostDelight.com
*/
use Zend\Telephony\Asterisk\Exception\CommunicationException;

class Agi {

const DEBUG_IN = '<';
const DEBUG_OUT = '>';
const DEBUG_INFO = '=';
const DEBUG_ERROR = '!';

/**
* Array containing the info about the session.
*
* Example:
* Array
* (
[agi_request] => /home/user/domains/domain.com/bin/AGI.php
[agi_channel] => SIP/phone1-00000009
[agi_language] => nl
[agi_type] => SIP
[agi_uniqueid] => 1300696790.9
[agi_version] => 1.8.3.2
[agi_callerid] => phone1
[agi_calleridname] => Dolf Schimmel (HostDelight)
[agi_callingpres] => 0
[agi_callingani2] => 0
[agi_callington] => 0
[agi_callingtns] => 0
[agi_dnid] => 911
[agi_rdnis] => unknown
[agi_context] => demo
[agi_extension] => 121212
[agi_priority] => 1
[agi_enhanced] => 0.0
[agi_accountcode] =>
[agi_threadid] => 1107945792
)
*/
protected $sessionInfo = array();

private $stdinStream;

private $stdoutStream;

protected $debugStream;

/**
*
* After a command is issued Asterisk returns a result code an message.
* These are stored in this property:
* array('code' => int, 'message' => string, 'data' => array|null, 'success' => bool)
* @var array
*/
protected $lastResult;

public function __construct($options = array(), $agiVars = null) {
$this->setOptions($options);
$this->writeDebug(self::DEBUG_INFO, 'AGI component initiated');

if($this->stdinStream === null) {
$this->stdinStream = fopen('php://stdin', 'r');
}

if($this->stdoutStream === null) {
$this->stdoutStream = fopen('php://stdout', 'w');
}

if($agiVars == null) {
$agiVars = $this->readVars();
}

$this->setSessionInfo($agiVars);

}

public function __destruct() {
if($this->stdoutStream) {
$this->hangup();
}

$this->writeDebug(self::DEBUG_INFO, 'AGI component exited');
}

protected function setOptions($options) {
foreach($options as $key => $value) {
switch(strtolower($key)) {
case 'debugfile':
$value = fopen((string)$value, 'a');
// Break left out intentionally
case 'debugstream':
$this->debugStream = $value;
break;
default:
trigger_error('Unknown option specified: '.$key, E_USER_NOTICE);
}
}
}

protected function setSessionInfo(array $vars) {
$this->sessionInfo = $vars;
}

public function writeDebug($type, $string) {
if($this->debugStream) {
return fwrite($this->debugStream, $type .' '. $string . PHP_EOL);
}
}

protected function write($string)
{

$this->writeDebug(self::DEBUG_OUT, $string);

if(!fwrite($this->stdoutStream, trim($string) . PHP_EOL)) {
$this->writeDebug(self::DEBUG_ERROR, 'Error while writing the above line to stream');
throw new CommunicationException('Could not write to stream: '.$string);
}

fflush($this->stdoutStream);

return $this;
}

/**
*
* Read input from the inputstream
* @param int $maxLines The max. amount to read
* @return array
*/
protected function readStream($maxLines = 0)
{
$out = array();
for($i = 1; (!feof($this->stdinStream) &&
(!$maxLines || $maxLines >= $i)); $i++)
{
$line = trim(fgets($this->stdinStream));
if ($line === '') {
break;
}

$this->writeDebug(self::DEBUG_IN, $line);
$out[] = $line;
}

return $out;
}

protected function readVars() {
$agiVars = array();

$streamInput = $this->readStream();
foreach($streamInput as $line) {
$agiVar = explode(':', $line);
$agiVars[$agiVar[0]] = trim($agiVar[1]);
}

return $agiVars;
}

/**
* @todo This method needs further tweaking.
*
* The AGI script communicates with Asterisk by sending AGI commands on
* standard output and receiving responses on standard input. The result
* typically takes this form: <code> result=<result> [data]
* where code is an HTTP-like response code (200 for success, 5xx for error)
* Result is the result of the command (common values are -1 for error, and
* 0 for success); some commands return additional name=value pairs in data,
* while some return a string value in parentheses (especially "timeout"
* for a timed command.) Also note that if code is followed by a hyphen
* instead of a space, the response will span multiple lines; the last line
* of the response will start with code. The response is easily defined
* using a regular expression:
*/
protected function registerResult() {
$raw = $this->readStream(1);

$code = substr($raw[0], 0, 3);
$multiline = substr($raw[0], 3, 4)=='-';
if($multiline) {
throw new Exception(
'A multiline response was received but is not supported (yet)'
);
}

$result = array('code' => $code);
if(substr($raw[0], 4, 7) == 'result=' && substr($raw[0], 12, 1) != '-') {
$result['success'] = true;
$result['message'] = substr($raw[0],4);
$result['data'] = array();
} else {
$result['success'] = false;
$result['message'] = substr($raw[0], 4);
$result['data'] = null;
}

$this->lastResult = $result;
return $result['success'];
}

public function getLastResult()
{
return $this->lastResult;
}

public function answer()
{
$this->write('ANSWER');
return $this->registerResult();
}

public function exec($command)
{
$this->write('EXEC ' . $command);
return $this->registerResult();
}

public function hangup($channelName = null)
{
$this->write('HANGUP' . $channelName);
$this->writeDebug(self::DEBUG_INFO, print_r($this->read(),1));
}

public function getAccountCode() {
if(!isset($this->sessionInfo['agi_accountcode'])) {
throw new Exception\RuntimeException(
'The AccountCode of the channel was tried to retrieve from '
. 'the session-info but it wasn\'t set.'
);
}

return $this->sessionInfo['agi_accountcode'];
}

public function getAsteriskVersion() {
if(!isset($this->sessionInfo['agi_version'])) {
throw new Exception\RuntimeException(
'The Asterisk Version was tried to retrieve from '
. 'the session-info but it wasn\'t set.'
);
}

return $this->sessionInfo['agi_version'];
}

public function getCallerId() {
if(!isset($this->sessionInfo['agi_callerid'])) {
throw new Exception\RuntimeException(
'The caller-id was tried to retrieve from '
. 'the session-info but it wasn\'t set.'
);
}

return $this->sessionInfo['agi_callerid'] != 'unknown'
? $this->sessionInfo['agi_callerid']
: null;
}

public function getCallerIdName() {
if(!isset($this->sessionInfo['agi_calleridname'])) {
throw new Exception\RuntimeException(
'The caller-id name was tried to retrieve from '
. 'the session-info but it wasn\'t set.'
);
}

return $this->sessionInfo['agi_calleridname'] != 'unknown'
? $this->sessionInfo['agi_calleridname']
: null;
}

public function getChannelId() {
if(!isset($this->sessionInfo['agi_channel'])) {
throw new Exception\RuntimeException(
'The Channel id was tried to retrieve from the session-info but it wasn\'t set.'
);
}

return $this->sessionInfo['agi_channel'];
}

public function getContext() {
if(!isset($this->sessionInfo['agi_context'])) {
throw new Exception\RuntimeException(
'The context was tried to retrieve from the session-info but none was set.'
);
}

return $this->sessionInfo['agi_context'];
}

public function getDialedNumber() {
if(!isset($this->sessionInfo['agi_dnid'])) {
throw new Exception\RuntimeException(
'The Dialed Number was tried to retrieve from the session-info but it wasn\'t set.'
);
}

return $this->sessionInfo['agi_dnid'];
}

public function getExtension() {
if(!isset($this->sessionInfo['agi_extension'])) {
throw new Exception\RuntimeException(
'The extension was tried to retrieve from the session-info but none was set.'
);
}

return $this->sessionInfo['agi_extension'];
}

public function getLanguage() {
if(!isset($this->sessionInfo['agi_language'])) {
throw new Exception\RuntimeException(
'The language was tried to retrieve from the session-info but it wasn\'t set.'
);
}

return $this->sessionInfo['agi_language'];
}

/**
*
* Get the priority in the dialplan
* @throws Exception\RuntimeException
*/
public function getPriority() {
if(!isset($this->sessionInfo['agi_priority'])) {
throw new Exception\RuntimeException(
'The priority in the dialplan was tried to retrieve '
. 'from the session-info but none was set.'
);
}

return $this->sessionInfo['agi_context'];
}

public function getSessionInfo() {
return $this->sessionInfo;
}

public function getThreadId() {
if(!isset($this->sessionInfo['agi_threadid'])) {
throw new Exception\RuntimeException(
'The thread id of the channel was tried to retrieve from '
. 'the session-info but it wasn\'t set.'
);
}

return $this->sessionInfo['agi_threadid'];
}

}

{code}
{deck}
{zone-data}


{zone-template-instance}]]></ac:plain-text-body></ac:macro>