20.4. Beispiele in verschiedenen Programmiersprachen

Im Folgenden werden AGI-Programme in einigen exemplarischen Programmiersprachen erklärt.

Perl

In der Standard-Asterisk-Installation ist ein Test-AGI-Skript mit dem Namen agi-test.agi [110]im Verzeichnis /var/lib/asterisk/agi-bin/ abgespeichert. Anhand dieses Perl-Programms werden wir die grundsätzliche Arbeit mit AGI-Skripten beschreiben.
Das Skript wird dabei in der extensions.conf wie folgt aufgerufen:
exten => 1234,1,Answer()
exten => 1234,2,AGI(agi-test.agi)
exten => 1234,3,Hangup()

Schritt-für-Schritt-Analyse des agi-test.agi-Skripts

Wir besprechen das Beispiel-Skript zeilen- oder abschnittsweise.
#!/usr/bin/perl
use strict;
Die ersten zwei Zeilen sagen dem ausführenden Betriebssystem, dass es sich um ein Perl-Programm handelt, das mit dem Interpreter /usr/bin/perl ausgeführt werden soll. use strict bewirkt eine konsequentere Behandlung von Fehlern innerhalb des Perl-Programms.
$|=1;
Diese kleine Zeile bringt Perl dazu, die Ausgabe von Text nicht zu puffern. So können wir sicher sein, dass alle Ausgaben auch unmittelbar an Asterisk übergeben und nicht erst in einem Buffer zwischengespeichert werden.
# Setup some variables
my %AGI; my $tests = 0; my $fail = 0; my $pass = 0;
Hier werden verschiedene Variablen definiert. Das Hash %AGI nimmt die initialen Eingaben von Asterisk auf. Die restlichen Variablen zählen die Gesamtanzahl der Tests, die Anzahl der fehlgeschlagenen Tests und die Anzahl der funktionierenden Tests.
while(<STDIN>) {
  chomp;
  last unless length($_);
  if (/^agi_(\w+)\:\s+(.*)$/) {
    $AGI{$1} = $2;
  }
}
Die eben eingelesenen Werte werden zum Debuggen auf STDERR, also im Endeffekt auf dem CLI ausgegeben:
print STDERR "AGI Environment Dump:\n";
foreach my $i (sort keys %AGI) {
        print STDERR " -- $i = $AGI{$i}\n";
}
Danach geht es mit checkresult weiter:
sub checkresult {
  my ($res) = @_;
  my $retval;
  $tests++;
  chomp $res;
  if ($res =~ /^200/) {
    $res =~ /result=(-?\d+)/;
    if (!length($1)) {
      print STDERR "FAIL ($res)\n";
      $fail++;
    } else {
      print STDERR "PASS ($1)\n";
      $pass++;
    }
  } else {
    print STDERR "FAIL (unexpected result '$res')\n";
    $fail++;
  }
}
Die Subroutine checkresult liest das Ergebnis eines Befehls an Asterisk aus und bestimmt, ob der Test erfolgreich war oder nicht. Entsprechend werden die Variablen $fail und $pass hochgezählt. Nachdem die Grundlagen gelegt sind, können die einzelnen Tests beginnen: Die Datei beep.gsm wird abgespielt.
print STDERR "1.  Testing 'sendfile'...";
print "STREAM FILE beep \"\"\n";
my $result = <STDIN>;
&checkresult($result);
Der Text "hello world" wird an den Anrufer geschickt. Das funktioniert natürlich nur, wenn das Protokoll und das Endgerät diese Funktion unterstützen.
print STDERR "2.  Testing 'sendtext'...";
print "SEND TEXT \"hello world\"\n";
my $result = <STDIN>;
&checkresult($result);
Das Bild "asterisk-image" wird an den Anrufer geschickt. Auch diese Funktion ist vom Protokoll und dem Endgerät abhängig.
print STDERR "3.  Testing 'sendimage'...";
print "SEND IMAGE asterisk-image\n";
my $result = <STDIN>;
&checkresult($result);
Die Zahl 192837465 wird dem Anrufer vorgelesen:
print STDERR "4.  Testing 'saynumber'...";
print "SAY NUMBER 192837465 \"\"\n";
my $result = <STDIN>;
&checkresult($result);
Dieser Befehl wartet 1000 Millisekunden auf die Eingabe von DTMF-Tönen durch den Anrufer:
print STDERR "5.  Testing 'waitdtmf'...";
print "WAIT FOR DIGIT 1000\n";
my $result = <STDIN>;
&checkresult($result);
Ein 3000 Millisekunden langes GSM-Soundfile mit dem Namen testagi.gsm wird aufgenommen. Die Aufnahme kann durch die Eingabe der Zahlen 1, 2, 3 oder 4 unterbrochen werden.
print STDERR "6.  Testing 'record'...";
print "RECORD FILE testagi gsm 1234 3000\n";
my $result = <STDIN>;
&checkresult($result);
Das soeben aufgenommene Soundfile wird abgespielt:
print STDERR "6a.  Testing 'record' playback...";
print "STREAM FILE testagi \"\"\n";
my $result = <STDIN>;
&checkresult($result);
Nun erfolgt die Ausgabe auf dem CLI, wie viele Tests funktioniert oder nicht funktioniert haben:
print STDERR "================== Complete ======================\n";
print STDERR "$tests tests completed, $pass passed, $fail failed\n";
print STDERR "==================================================\n";

Warnung

Bei vielen AGI-Befehlen sehen Sie den folgenden Aufbau:
fwrite(STDOUT,"BEFEHL $value \"\"\n");
#                            ^^^^^^^
Der in dieser Zeile unterschlängelte Teil (zwischen $value und );) ist zwingend erforderlich, damit der Befehl korrekt ausgeführt wird. Es handelt sich hierbei um ein Argument ohne Inhalt, das durch zwei gequotete Anführungszeichen dargestellt wird. Abgeschlossen wird der gesamte Befehl durch ein line feed (ein Zeilenende-Zeichen), in diesem Fall ein \n.

PHP

PHP ist zu einer der populärsten Programmiersprachen für Webapplikationen geworden.[111] Da man aber aktuelle PHP-Versionen auch für den Aufruf von Programmen auf der Kommandozeile benutzen kann, ist PHP eine für AGI-Skripte gut geeignete Sprache. Als Beispiel benutzen wir ein kleines PHP-Programm (lotto.php), das 6 zufällige Zahlen von 1 bis 49 auswählt und dem Anrufer vorspricht. Die Beschreibung der einzelnen Schritte erfolgt im Quellcode.
#!/usr/bin/php -q
<?php

# Sicherheitseinstellung. Das Skript läuft nicht 
# laenger als 8 Sekunden.
#################################################
set_time_limit(8);


# Output Buffer wird deaktiviert
# Alternativ könnten wir nach jeder Ausgabe
# fflush(STDOUT); aufrufen.
#################################################
ob_implicit_flush();


# PHP Error Reporting wird deaktiviert
#################################################
error_reporting(0);


# Für die Kommunikation mit Asterisk benötigen 
# wir STDIN und STDOUT Filehandles
#################################################
if (!defined('STDIN'))
  define('STDIN' , fopen('php://stdin' , 'r'));
if (!defined('STDOUT'))
  define('STDOUT', fopen('php://stdout', 'w'));
if (!defined('STDERR'))
  define('STDERR', fopen('php://stderr', 'w'));


# Die von Asterisk übergebenen Variablen und 
# Werte werden ausgelesen und im Array $agi 
# gespeichert.
#################################################

$agi = array();

while (!feof(STDIN))
{
  $tmp = trim(fgets(STDIN,4096));
  if (($tmp == '') || ($tmp == "\n"))
    break;
  $var1 = split(':',$tmp);
  $name = str_replace('agi_','',$var1[0]);
  $agi[$name] = trim($var1[1]);
}


# Ein Array mit 6 zufälligen und nicht 
# doppelten Zahlen von 1 bis 49 wird generiert.
#################################################

$Lottozahlen = array();
do {
  $Zahl = rand(1,49);
  if (array_search($Zahl, $Lottozahlen) == FALSE) {
    $Lottozahlen[] = $Zahl;
  }
} while (count($Lottozahlen) < 6);


# Vor der ersten Ansage wird eine Sekunde 
# gewartet.
#################################################
fwrite(STDOUT,"EXEC Wait 1 \"\"\n");
fflush(STDOUT);


# Die Zahlen werden nacheinander vorgelesen. 
# Zwischen den einzelnen Zahlen gibt es immer 
# eine Pause von einer Sekunde.
#################################################
foreach ($Lottozahlen as $value) {
  fwrite(STDOUT,"SAY NUMBER $value \"\"\n");
  fflush(STDOUT);
  fwrite(STDOUT,"EXEC Wait 1 \"\"\n");
  fflush(STDOUT);
}

?>
Das Programm lotto.php muss im Verzeichnis /var/lib/asterisk/agi-bin/ abgespeichert werden und wird in der extensions.conf wie folgt aufgerufen:
exten => 1234,1,Answer()
exten => 1234,2,AGI(lotto.php)
exten => 1234,3,Hangup()

phpAGI

Wenn Sie PHP und AGI einsetzen, aber nicht das Rad neu erfinden wollen, können Sie die fertige PHP-Klasse phpAGI benutzen. Informationen und die dazugehörigen Dateien finden Sie auf der Homepage des Projekts http://phpagi.sourceforge.net/.
Das folgende mit phpAGI mitgelieferte Programm gibt schnell einen Überblick über die Funktionsaufrufe[112]
<?php
/**
* @package phpAGI_examples
* @version 2.0
*/

function my_ip(&$agi, $peer)
   {
    $ip = 'unknown';
    $asm = $agi->new_AsteriskManager();
    if($asm->connect())
    {
      $peer = $asm->command("sip show peer $peer");
      $asm->disconnect();
    
      if(!strpos($peer['data'], ':'))
        echo $peer['data'];
      else
      {
        $data = array();
        foreach(explode("\n", $peer['data']) as $line)
        {
          $a = strpos('z'.$line, ':') - 1;
          if($a >= 0) $data[trim(substr($line, 0, $a))] = trim(substr($line, $a + 1));
        }
      }
    
      if(isset($data['Addr->IP']))
      {
        $ip = explode(' ', trim($data['Addr->IP']));
        $ip = $ip[0];
      }
    }
    $agi->text2wav("Your IP address is $ip");
  }
?>

Ruby und Adhearsion

Obwohl Ruby eine für EDV-Verhältnisse alte Programmiersprache ist, hat sie erst in den letzten Jahren (hauptsächlich durch Ruby on Rails) eine große Popularität erlangt. Adhearsion ist für Ruby und Asterisk dabei das Gleiche, was Ruby on Rails für Ruby und Webapplikationen ist. Das Thema ist dabei so spannend, dass man darüber ein eigenes Buch schreiben könnte. Auch hier werden wir im Laufe der Zeit auf der Webseite zum Buch Updates nachreichen.

Installation

Wie es sich für ein gutes Open-Source-Projekt gehört, ist auch die Dokumentation von Adhearsion reichlich verzweigt und oft unvollständig. Glücklicherweise ist wenigstens die Installation denkbar einfach, weil es zu Adhearsion ein fertiges gem gibt (für Ruby-Neulinge: gem ist ein Paketmanagement-Mechanismus für Ruby). Entsprechend kann die Installation mit gem install adhearsion vollzogen werden.

Einfaches Setup

Mit der Benutzung von Adhearsion wird der Dialplan von Asterisk recht übersichtlich und kurz. Man muss nur bei jedem eingehenden Context die folgende Zeile einfügen:
exten => _.,1,AGI(agi://127.0.0.1)
Damit übernimmt Adhearsion die Kontrolle über alle Gespräche, die in diesem Context geführt werden. Natürlich kann man aber auch einen traditionellen Dialplan mit Adhearsion mischen.
Eine Adhearsion-Applikation muss ähnlich wie bei Ruby on Rails erst generiert werden. Dies erfolgt mit dem Programm ahn, das mit ahn create applikationsname aufgerufen wird. Beispiel:
stefan@pbx:~$ ahn create apfelmus_app
      create  
      create  components/simon_game
      create  components/disabled/stomp_gateway
      create  components/ami_remote
      create  components/restful_rpc/spec
      create  config
      create  .ahnrc
      create  components/simon_game/simon_game.rb
      create  components/ami_remote/ami_remote.rb
      create  components/disabled/stomp_gateway/stomp_gateway.rb
      create  components/disabled/stomp_gateway/config.yml
      create  components/disabled/stomp_gateway/README.markdown
      create  components/restful_rpc/restful_rpc.rb
      create  components/restful_rpc/config.yml
      create  components/restful_rpc/README.markdown
      create  components/restful_rpc/example-client.rb
      create  components/restful_rpc/spec/restful_rpc_spec.rb
      create  config/startup.rb
      create  dialplan.rb
      create  events.rb
      create  README
      create  Rakefile
stefan@pbx:~$ cd apfelmus_app
stefan@pbx:~/apfelmus_app$ 
Gestartet wird die Applikation dann mit ahn start .
stefan@pbx:~/apfelmus_app$ ahn start .
 INFO ahn: Adhearsion initialized!
Ab diesem Zeitpunkt übernimmt Adhearsion die Kontrolle über Anrufe. Sie können dabei auf dem Bildschirm mitverfolgen, was passiert.

Beispielprogramm

Der Adhearsion-Dialplan oder besser gesagt das Ruby-Programm wird in der Datei dialplan.rb definiert. Hier ein einfaches Beispiel:
intern {
  case extension
  when 22
    play "hello-world"
    hangup
  else
    dial "SIP/#{extension}"
  end
}
intern ist in diesem Beispiel der benutzte Context, aus dem Adhearsion aufgerufen wurde.

Andere Programmiersprachen

Wie am Anfang dieses Kapitels schon erwähnt wurde, kann man AGI-Programme in jeder beliebigen Programmiersprache schreiben. Fertige Bibliotheken finden sich z. B. für:
  • Java
  • Perl
  • PHP
  • Python
  • Ruby
  • C
Am einfachsten ist eine Suche in der Suchmaschine Ihrer Wahl nach den Schlüsselwörtern AGI und dem Namen der Programmiersprache.


[110] Die Dateiendung agi ist dabei nicht zwingend notwendig. Man könnte die Datei auch agi-test.pl nennen.

[111] Böse Zungen sprechen auch vom BASIC des 21. Jahrhunderts. ;-)

[112] Für die Benutzung von text2wav muss ein Text-to-Speech-System (z. B. Festival) installiert und konfiguriert sein.