Asterisk und Jajah!

Aus NOBAQ
Wechseln zu: Navigation, Suche
Jajaah.gif

Wirklich schade, dass es peterzahlt.de nicht für Österreich gibt! So eine Einbindung in Asterisk wär ja wirklich spannend zu machen. Aber dafür hab ich etwas von Jajah gehört (ursprünglich offenbar ein Konkurrenzprodukt zu Skype). Auf der Homepage gibt man seine eigene Nummer an, die Zielnummer und man wird zurück gerufen. Wär einfach interessant, das in Asterisk zu implementieren. Und so hab ich zu hacken begonnen. Ein AGI-Script loggt sich über LWP::UserAgent in Jajah ein, und fordert den Rückruf an.

Und genau da kommt das große Problem: Der Rückruf kommt sehr schnell! Im Regelfall geht sich nicht einmal ein Hangup() aus. So ist für den eingehenden “Callback”-Anruf die Nebenstelle blockiert und er landet im Anrufbeantworter. Nicht schön. Gesucht wäre also eine Möglichkeit, einen eingehenden Anruf mit einem geantworteten Channel zu verbinden. Dazu hab ich aber leider nichts gefunden :-(. Aber über einen kleinen Hack gehts trotzdem...


Um ehrlich zu sein, hab ich dieses Problem am Anfang gar nicht bedacht und einfach einmal angefangen, das Parser-Script zu hacken. Erst beim Testen kam ich drauf, dass ich da was vergessen hatte.

Da es offenbar nicht möglich ist, zwei angenommene Channels zu verbinden, kam mir eine Idee: MeetMe() brauch ich ja sonst zu nix, aber da könnte es sich als nützlich erweisen! Bingo!

Vorgangsweise: Man will jemand anrufen. Asterisk überprüft, ob Jajah für den Anruf die günstigste Methode ist und ruft das AGI-Script auf. Dieses loggt sich ein und fordert den Callback an. Während des AGI-Scripts wird das ring-Signal eingespielt. Sofort danach wird ein Konferenzraum mit MeetMe() geöffnet, der solange MoH (Music On Hold) spielt, bis der “markierte” Anruf den Raum betritt. Dann sind die beiden Gespräche verbunden.

Glücklicherweise (?) spooft Jajah die CallerID, sodass der Callback-Anruf die CallerID meiner Wunschdestination hat. Also speichere ich kurz nach dem AGI-Script einen Unix-Timestamp mit der passenden CallerID in die Datenbank (AstDB).

Kommt nun ein Anruf herein, wird überprüft, ob ein Eintrag in der Datenbank existiert. Falls ja, wird er auf alle Fälle gelöscht. Ist dieser in einem Toleranzzeitraum (z.B. 1 Minute, es wird ja der Timestamp gespeichert), wird ebenfalls als markierte Person MeeMe() aufgerufen mit dem Timestamp als Konferenzraum-ID. Die Anrufer sind verbunden und alles ist normal. Ist das Timeout abgelaufen, so wird ganz einfach der Anruf weitergegeben.

Zusätzlich setzt das AGI-Script eine Variable JAJAH_STATUS. Damit ist es möglich zu prüfen, ob der Anruf ergolgreich war. Falls nein, kann als Fallback ein anderes Netz gewählt werden.

Anfangs hatte ich das ganze als komplizierten Dialplan drinnen und das AGI-Script übernahm nur das Parsing. Mittlerweile hab ich alles in das AGI-Script verpackt und es reichen wenige Funktionen im Dialplan

Seit neustem verwende ich zum Glück (!) AEL, sodass ich die Fragmente hier nur mehr als AEL-Code angib. Im Endeffekt muss folgendes Makro hinzugefügt werden:

macro isJajahCallback() {
	AGI(jajah.agi,in);
};

Man beachte den Parameter “in” für den eingehenden Rückruf. In den Wählplan wird das im Input-Trunk einfach eingebunden:

context capi-in {
	s => {
		&hangupWhenChanBusy();
		&isJajahCallback();
		[...]
	};
};

Für das Wählen legt man sich am besten einen eigenen Context an, zu dem man bei Bedarf springt:

context jajah {
	_9X. => {
		NUM=${EXTEN:1};
		JAJAH_USER=jajah_username;
		JAJAH_PASS=jajah_password;
		JAJAH_NUMBER=registered_jajah_number;
		AGI(jajah.agi,out,${NUM});
		if(${JAJAH_STATUS} != 0) {
			// Hier gehört stattdessen eine Fallbackbehandlung her!
			Answer();
			SayNumber(${JAJAH_STATUS});
			Playtones(info);
			Wait(60);
			StopPlaytones();
			Hangup();
		};
 
	t => {
		Playtones(info);
		Wait(60);
		StopPlaytones();
		Hangup();
		};
};

Wiederum beachte man den zweiten Parameter “out” für “Herauswahl”. Das ist schon das einzige, was man im Dialplan machen muss. Den Rest erledigt das AGI-Script! Es müdden halt die Module für die Datenbank und MeetMe vorhanden und geladen sein.

Der Hauptcode des AGI-Scripts sieht so aus:

#!/usr/bin/perl
 
# Asterisk --> Jajah Gateway
# copyright 2006 nikolaus hammler
# licensed under GNU GPL
 
 
use strict;
use LWP::UserAgent;
#use LWP::Debug qw(+);
use HTML::Parser;
use HTTP::Cookies;
use HTTP::Request::Common qw(POST);
use Asterisk::AGI;
 
my $AGI = new Asterisk::AGI;
 
my %input = $AGI->ReadParse();
 
if($ARGV[0] eq "in")
{
  print STDERR "Dialin mode\n";
 
  my $exten = $input{'callerid'};
 
  my $id = $AGI->get_variable("DB(jajah/$exten)");
  if($id)
  {
    print STDERR "Yes, our callback. exten=$exten, id=$id\n";
 
    $AGI->exec('DBDel', "jajah/$exten");
 
    if(time() - $id < 120)
    {
      $AGI->exec('Answer');
      $AGI->exec('MeetMe', "$id|Aqd");
      $AGI->exec('Hangup');
    }
    else
    {
      print STDERR "Warning: time's up!\n";
      exit 0;
    }
  }
  else
  {
    print STDERR "Maybe not our callback, ignoring\n";
    exit 0;
  }
}
elsif($ARGV[0] ne "out")
{
  print STDERR "First argument (mode) must either be 'in' or 'out'\n";
  $AGI->exec('Set', 'JAJAH_STATUS=8');
  exit 0;
}
 
print STDERR "Dialout mode\n";
 
my $UserAgent = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rev:1.8.1.1) Gecko/20061204 Firefox/2.0.0.1';
 
my $ua = LWP::UserAgent->new();
 
$ua->agent($UserAgent);
 
$ua->cookie_jar(HTTP::Cookies->new);
 
my $username = $AGI->get_variable('JAJAH_USER');
my $password = $AGI->get_variable('JAJAH_PASS');
my $jajahnum = $AGI->get_variable('JAJAH_NUMBER');
 
print STDERR "Jajah Username: $username\n";
print STDERR "Jajah Password: $password\n";
print STDERR "Jajah Number: $jajahnum\n";
 
if(!$username)
{
  print STDERR "No username given. Please set variable JAJAH_USER\n";
  $AGI->exec('Set', 'JAJAH_STATUS=1');
  exit 0;
}
if(!$password)
{
  print STDERR "No password given. Please set variable JAJAH_PASS\n";
  $AGI->exec('Set', 'JAJAH_STATUS=2');
  exit 0;
}
if(!$jajahnum)
{
  print STDERR "No jajah number given. Please set JAJAH_NUMBER\n";
  $AGI->exec('Set', 'JAJAH_STATUS=6');
  exit 0;
}
 
my $exten = $input{'agi_extension'};
 
if($ARGV[1] ne "")
{
  $exten = $ARGV[1];
}
 
print STDERR "ToDial: $exten\n";
 
if(!$exten)
{
  print STDERR "Don't know what to dial, sorry\n";
  $AGI->exec('Set', 'JAJAH_STATUS=3');
  exit 0;
}
 
my $area = '43';
my $number = substr($exten, 1);
 
if(substr($exten, 0, 2) eq '00')
{
  # fixxme: Was is wenn vorwahl 3-stellig?
  $area = substr($exten, 2, 2);
  $number = substr($exten, 4);
}
elsif(substr($exten, 0, 1) ne '0')
{
  print STDERR "Wrong format. Pleas use prefix 0 for national and prefix 00 for international calls\n";
  $AGI->exec('Set', 'JAJAH_STATUS=5');
  exit 0;
}
 
$AGI->exec('Answer');
$AGI->exec('Playtones', 'ring');
 
my $html = &PostFormInHTML('http://www.jajah.com/mini/login.aspx',
  'theForm',
  { 'ctl00$MainContent$Email' => $username,
    'ctl00$MainContent$Password' => $password});
 
if($html =~ /Successfully logged in/)
{
  print STDERR "Login success. Trying to dial...\n";
 
  $html = &PostFormInHTML('http://www.jajah.com/mini/member.aspx?',
  #$html = &PostFormInHTML('http://www.google.com',
    'theForm',
    {'mynumber' => $jajahnum,
     'ctl00$MainContent$CallTo$minidialcode' => $area,
     'ctl00$MainContent$CallTo$mininumber' => $number});
 
  if($html =~ /Active call/)
  {
    print STDERR "Everything seems fine, callback should come shortly\n";
    $AGI->exec('Set', 'JAJAH_STATUS=0');
 
    my $id = time();
 
    $AGI->exec('Set', "DB(jajah/$exten)=$id");
    $AGI->exec('StopPlaytones');
    $AGI->exec('MeetMe', "$id|1Mdqwx");
 
    $AGI->exec('Hangup');
 
    exit 0;
  }
  else
  {
    if(open(DEBUG, ">/tmp/asterisk_jajah_debug.html"))
    {
      print DEBUG $html;
      close(DEBUG);
    }
    print STDERR "Call failed. Maybe you can find debug info in /tmp/asterisk_jajah_debug.html\n";
    $AGI->exec('Set', 'JAJAH_STATUS=7');
    $AGI->exec('StopPlaytones');
  }
}
else
{
  print STDERR "Login failed\n";
  $AGI->exec('Set', 'JAJAH_STATUS=4');
  $AGI->exec('StopPlaytones');
  exit 0;
}
 
sub WebRequest()
{
    my ($url) = @_;
    my $req;
    my $res;
 
    $req = HTTP::Request->new('GET', $url);
 
    $res = $ua->request($req);
 
    if($res->is_error())
    {
      return "";
    }
    return $res->content();
}
 
sub WebPost($;\%;$;)
{
    my ($url, $data, $referer) = @_;
 
    #my $ua;
    my $req;
    my $res;
    my $cookie;
 
    $req = POST $url, $data;
 
    if($referer ne "")
    {
	$req->referer($referer);
    }
    $res = $ua->request($req);
    if($res->is_error())
    {
      return "";
    }
 
    if(exists($res->headers->{'location'})) {
	return &WebRequest(&UrlGlue($res->headers->{'location'}, $url));
    }
    return $res->content();
}
 
sub UrlGlue()
{
    my ($url, $orig) = @_;
    my $LastDir = substr($orig, 0, rindex($orig, "/"));
    # Get server, if no complete URI is given...
    my $Server = substr($orig, 0, index(substr($orig, 7), "/")+7);
 
    # Niki, 051104
    # one geht nicht, hat offensichtlich auf https umgestellt.
    # handle also also https URLs...hoffentlich
    if(substr($orig, 0, 5) eq "https")
    {
	$Server = substr($orig, 0, index(substr($orig, 8), "/")+8);
    }
 
    #print "URL: '$url'\n";
    #print "LastDir: '$LastDir'\n";
    #print "Server: '$Server'\n";
 
    if(substr($url, 0, 1) eq "/")
    {
        # root on server, just add the server!
        $url = $Server . $url;
    }
    elsif(substr($url, 0, 4) ne "http")
    {
        # complete relative path...
        $url = $LastDir . "/" . $url;
    }
    return $url;
}
 
sub GetFilename()
{
    my ($url) = @_;
 
    return substr($url, rindex($url, "/")+1);
}
 
################################### FORMULAR FUNKTIONEN ################################
# Diese Funktionen erlauben es, einfach Zugriff auf ein HTML-Formular zu erlangen und
# abzuschicken, ohne es extra parsen zu muessen
 
my $Form_Name = "";		# Haelt den Namen des Formulars
my $Form_Jump = 0;		# Kann auch gegeben viele Formulare ueberspringen!
 
my $Form_Method = "";		# Sendemethode (GET, POST etc)
my $Form_Action = "";		# URL, auf die das Formular geschickt werden soll
my $Form_isOur = 0;		# BOOL: Ist true, wenn wir in unserem Formular sind
my $Form_Break = 0;		# BOOL: Ist true, wenn wir komplett mit unserem Formular fertig sind
 
# 10.05.2004
my $Form_Submit = "";		# Welches Submitfeld wird genommen? Wenn leer: Standard, sonst das was da drin steht
my $Form_isSubmit = 0;		# BOOL: true, wenn es bereits gefunden wurde!
my $Form_SubmitRegexp = 0;	# BOOL: Soll mit Hilfe von if ~ gesucht werden (falls z.B. auch der Name Parameter teilweise dynamisch erstellt wurde. Dann dem Parameter ein '~' vorstellen
 
my @Form_Elems = ();		# Haelt alle Formularelemente
my $Form_Elems_cnt = 0;		# ...und die Anzahl davon
 
my $Form_isTextarea = 0;	# BOOL: Befinden wir uns in einer Textarea?
my $Form_isSelect = 0;		# BOOL: Befinden wir uns in einer Combobox?
my $Form_CatchSelect = 0;	# BOOL: falls keine value="" gegeben ist,
				# muss zwischen <option> die value stehn...
my $Form_DefaultSelect = "";	# Wenn kein "selected" gegeben, nimm das erste Element
 
# Ruft eine Seite auf, die ein Formular enthaelt, und schickt dieses Formular
# wiederum ab. Gibt die HTML Seite der Zielseite als Skalar zurueck.
# 1. Parameter: URL der Formularseite
# 2. Parameter: Der Formularname, oder das wievielte Formular auf der Seite uns'res ist
# 3. Parameter: Hash-Pointer auf die Elemente, die ausgefuellt/ersetzt werden sollen
# return-Value: HTML Code der Zielseite AS string
 
sub PostFormInHTML()
{
    my ($url, $formname, $elems, $submitWith) = @_;
    my $PageContent;
    $PageContent = &WebRequest($url);
    return &__PostFormInHTML($PageContent, $url, $formname, $elems, $submitWith);
}
 
sub PostFormInHTMLCode()
{
    my ($PageContent, $url, $formname, $elems, $submitWith) = @_;
    return &__PostFormInHTML($PageContent, $url, $formname, $elems, $submitWith);
}
 
sub __PostFormInHTML()
{
    my ($PageContent, $url, $formname, $elems, $submitWith) = @_;
    #my $PageContent;
    my $p;
    my $i;
    my %PostData;
 
    # Hole mittels normalem GET Request die URL...
    #$PageContent = &WebRequest($url);
 
    # Wenn 2. Parameter eine Zahl ist, ueberspringe soviel Formulare.
    # Wenn nicht, ist das der Name unseres Formulars
    if($formname =~ /^[0-9]+/)
    {
	$Form_Jump = ($formname * -1);
	$Form_Name = "";
    }
    else
    {
	$Form_Name = $formname;
	$Form_Jump = 0;
    }
 
    $Form_SubmitRegexp = 0;
    if($submitWith && length($submitWith) > 0)
    {
	if(substr($submitWith, 0, 1) eq "~")
	{
	    $submitWith = substr($submitWith, 1);
	    $Form_SubmitRegexp = 1;
	}
	$Form_Submit = $submitWith;
    } else {
	$Form_Submit = "";
    }
    $Form_isSubmit = 0;
 
    # Alle Variablen null setzen
    $Form_isOur = 0;
    $Form_Break = 0;
    $Form_Elems_cnt = 0;
    @Form_Elems = ();
    $Form_isTextarea = 0;
    $Form_isSelect = 0;
    $Form_CatchSelect = 0;
    $Form_DefaultSelect = "";
 
    # Parser anwerfen
    $p = HTML::Parser->new(api_version => 3,
	start_h => [\&ParseFormStart, "self,tagname,attr,text"],
	text_h => [\&ParseFormText, "text,offset"],
	end_h => [\&ParseFormEnd, "tagname"]);
    $p->parse($PageContent);
    $p->eof;
 
    # Uergebene Felder ersetzen / ausfuellen (ins Array)
    for($i = 0; $i < $Form_Elems_cnt; $i++)
    {
	my $key;
	my $value;
	while(($key, $value) = each(%{$elems}))
	{
	    if($key eq $Form_Elems[$i][0])
	    {
		$Form_Elems[$i][1] = $value;
	    }
	}
    }
 
    # Debug! Wenn's bis hier geschafft ist, ist alles getan!
    # Druckt *alle* fertigen Formularelemente aus, incl.
    # Values
    #for($i = 0; $i < $Form_Elems_cnt; $i++)
    #{
    #	print STDERR $Form_Elems[$i][0] . " --> " . $Form_Elems[$i][1] . "\n";
    #}
 
    # QND: Das ganze Array in einen Hash kopieren (unnoetig!)
    for($i = 0; $i < $Form_Elems_cnt; $i++)
    {
	$PostData{$Form_Elems[$i][0]} = $Form_Elems[$i][1];
    }
 
    # GET Requests werden noch nicht unterstuetzt!
    if($Form_Method =~ /[Gg][Ee][Tt]/)
    {
      # TODO
      return "";
    }
 
    # Falls die action keine komplette URL ist, die URL
    # komplettieren (mit http:// (...))
 
    if($Form_Action eq "")
    {
      $Form_Action = $url;
    }
    else
    {
      $Form_Action = &UrlGlue($Form_Action, $url);
    }
 
    #print STDOUT "FORM-ACTION: $Form_Action\n";
 
    # Den POST Request durchfuehren und den Inhalt der Seite zurueckgeben
    return &WebPost($Form_Action, \%PostData, $url);
}
 
sub DoInputAttr()
{
    my ($attr, $handle_form_submit) = @_;
    if($attr->{'name'})
    {
	if($attr->{'type'} eq "radio")
	{
	    my $iFound = 0;
	    for(my $i = 0; $i < $Form_Elems_cnt; $i++)
	    {
		if($Form_Elems[$i][0] eq $attr->{'name'})
		{
		    $iFound = $i;
		    last;
		}
	    }
 
	    # Wenn es das radio noch nicht in der Liste ist, fuege es hinzu
	    if(!$iFound)
	    {
		$Form_Elems[$Form_Elems_cnt][0] = $attr->{'name'};
		$Form_Elems[$Form_Elems_cnt][1] = $attr->{'value'};
		$Form_Elems_cnt++;
	    }
	    else
	    {
		# Wenn es gefunden wurde, aber gecheckt ist, ueberschreibe das Attribut
		if($attr->{'checked'})
		{
		    $Form_Elems[$iFound][1] = $attr->{'value'};
		}
	    }
	}
	else
	{
    	    $Form_Elems[$Form_Elems_cnt][0] = $attr->{'name'};
    	    $Form_Elems[$Form_Elems_cnt][1] = $attr->{'value'};
    	    $Form_Elems_cnt++;
	}
    }
    if($handle_form_submit && $attr->{'type'} eq "submit")
    {
        $Form_isSubmit = 1;
    }
}
 
sub ParseFormStart()
{
    my ($self, $tagname, $attr, $src) = @_;
    my $handle_form_submit = 0;
 
    if($Form_Break)
    {
	return;
    }
 
    if($tagname eq "form")
    {
	if($Form_Name ne "")
	{
	    if($attr->{'name'} eq $Form_Name)
	    {
		if($attr->{'method'})
		{
		    $Form_Method = $attr->{'method'};
		}
		else
		{
		    $Form_Method = "";
		}
		if($attr->{'action'})
		{
		    $Form_Action = $attr->{'action'};
		}
		else
		{
		    print STDERR "WARNING: Das gewaehlte Formular hat keine action-Eigenschaft!\n";
		    $Form_Action = '';
		}
		$Form_isOur = 1;
	    }
	}
	else
	{
	    $Form_Jump++;
	    if($Form_Jump > 0)
	    {
		if($attr->{'method'})
                {
                    $Form_Method = $attr->{'method'};
                }
                else
                {
                    $Form_Method = "";
                }
                if($attr->{'action'})
                {
                    $Form_Action = $attr->{'action'};
                }
                else
                {
                    die("Das gewaehlte Formular hat keine action-Eigenschaft!\n");
                }
		$Form_isOur = 1;
	    }
	}
    }
 
    if($Form_isOur)
    {
	if($tagname eq "input")
	{
	    if($Form_Submit ne "")
	    {
		if($Form_SubmitRegexp)
		{
		    if($attr->{'name'} =~ /$Form_Submit/)
		    {
		        $handle_form_submit = 1;
		    }
		    else
		    {
		        $handle_form_submit = 0;
		    }
		} else {
		    if($attr->{'name'} eq $Form_Submit)
		    {
		        $handle_form_submit = 1;
		    }
		    else
		    {
		        $handle_form_submit = 0;
		    }
		}
	    }
 
	    if($attr->{'type'} ne "submit" || $Form_Submit eq "" || ($handle_form_submit && $Form_isSubmit == 0))
	    {
		&DoInputAttr($attr, $handle_form_submit);
	    }
	}
	if($tagname eq "textarea")
	{
	    $Form_isTextarea = 1;
	    $Form_Elems[$Form_Elems_cnt][0] = $attr->{'name'};
	}
	if($tagname eq "select")
	{
	    $Form_isSelect = 1;
	    $Form_Elems[$Form_Elems_cnt][0] = $attr->{'name'};
	    $Form_Elems[$Form_Elems_cnt][1] = "";
	}
	if($Form_isSelect && $tagname eq "option")
	{
	    if($Form_DefaultSelect eq "")
	    {
	    	$Form_DefaultSelect = $attr->{'value'};
	    }
	    if($attr->{'selected'})
	    {
		if($attr->{'value'} ne "")
		{
		    $Form_Elems[$Form_Elems_cnt][1] = $attr->{'value'};
		}
		else
		{
		    $Form_CatchSelect = 1;
		}
	    }
	}
    }
}
 
sub ParseFormText()
{
    my ($dtext, $offset) = @_;
 
    if($Form_Break)
    {
	return;
    }
 
    if($Form_isOur)
    {
	if($Form_isTextarea)
	{
	    $Form_Elems[$Form_Elems_cnt][1] = $dtext;
	}
	if($Form_CatchSelect)
	{
	    $Form_Elems[$Form_Elems_cnt][1] = $dtext;
	    $Form_CatchSelect = 0;
	}
    }
}
 
sub ParseFormEnd()
{
    my ($tagname) = @_;
 
    if($Form_Break)
    {
	return;
    }
 
    if($Form_isOur)
    {
	if($tagname eq "form")
	{
	    $Form_isOur = 0;
	    $Form_Break = 0;
	}
	if($Form_isTextarea && $tagname eq "textarea")
        {
	    $Form_Elems_cnt++;
	    $Form_isTextarea = 0;
	}
	if($Form_isSelect && $tagname eq "select")
	{
	    if($Form_Elems[$Form_Elems_cnt][1] eq "")
	    {
	    	$Form_Elems[$Form_Elems_cnt][1] = $Form_DefaultSelect;
		$Form_DefaultSelect = "";
	    }
	    $Form_Elems_cnt++;
	    $Form_isSelect = 0;
	}
    }
}

Den ganzen Code hab ich als Attachment hinzugefügt.

Conclusio: Das ganze war lustig zum programmieren, aber verwendet hab ichs nie weil ich erst später draufgekommen bin, das Jajah nur so unwesentlich billiger ist, dass es sich gar nicht auszahlt. Media:jajahagi.txt

Kommentare

Diskussion:Asterisk und Jajah!
Meine Werkzeuge