A simple AGI scripting engine with Asterisk-Java

  Asterisk, JAVA
ntroduction
Asterisk, the well known free PBX, exposes great integration potentialities. Specifically , we’re interested in AGI (Asterisk Gateway Interface), a sort of API for scripting the engine.
Today we’ll face a java interface for AGI: asterisk-java. We’ll use this easy library to build a scripting engine. The used scripting language is BeanShell, but you can virtually use any language supported by the Sun jdk (python,ruby,groovy, javascript etc.).

Asterisk-Java and AGI – some code

The library asterisk-java is of immediate use; its simplest use is based on fastagi protocol, and though it also exposes a Manager API, fastagi will be enough for this article purposes . To build our scripting engine we just need to subclass “BaseAgiScript” and  override its “service” method. From the AgiRequest object we’ll grab the parameter we’re interested in, “script”, which is just the name of a beanshell script we want to execute. We’ll then pass a reference of our class, the AgiRequest and the AgiChannel object to the beanshell interpreter, launch a global script that defines some utility functions (see below) and finally launch the requested script. The approach is quite flexible, our script engine doesn’t need to be on the same computer the pbx is on, and we can add/modify our scripts on the fly without need for compilation or engine restart.

package org.beanizer.bagiserver;


import bsh.EvalError;
import java.io.FileNotFoundException;
import java.io.IOException;
import org.asteriskjava.fastagi.AgiChannel;
import org.asteriskjava.fastagi.AgiException;
import org.asteriskjava.fastagi.AgiRequest;
import org.asteriskjava.fastagi.BaseAgiScript;

import bsh.Interpreter;

public class BAgiServer extends BaseAgiScript{
    
    public BAgiServer() {
    }

    public void service(AgiRequest request, AgiChannel channel)
    throws AgiException {
        
        Interpreter interp=new Interpreter();
        try {
            String script=((request.getParameter("script")!=null) && !( request.getParameter("script").equals("") ) )
								? request.getParameter("script") : "../scriptlib/default";
            interp.source("scriptlib/bagi_import.bsh");
            interp.set("bagi",this);
            interp.set("request",request);
            interp.set("channel",channel);
            
            interp.source("scripts/"+ script +".bsh");
            
        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
        } catch (IOException ex) {
            ex.printStackTrace();
        } catch (EvalError ex) {
            ex.printStackTrace();
        }
    }
   
} 

A “fastagi-mapping.properties” file must exists on the classpath of the scripting engine and should contain something like this:

server.agi = BAgiServer

where “server.agi” is the agi script name called from asterisk, to be mapped to “BagiServer”, our class name.

We also need to add a call to our engine from asterisk’s dialplan, so in “extensions.conf” we’ll add something like:

exten => 1200,1,Agi(agi://localhost/server.agi?script=scriptname)

where “1200” can be an extension number of your choice, “localhost” is the server your scripting engine is hosted on, “server.agi” is the name we’ve mapped for our scripting engine and “scriptname” is the name of the beanshell script we want to invoke(without .bsh extension).

When someone will try to call extension “1200”, the specified agi script will be executed.

In our source code you can see we’re using two directories relative to the base classpath of BAgiServer:

 

  • “scriptlib”, wich contains scripts “default”(called if no script name is passed as parameter), and “bagi_import” ( where we put generic functions we can use from other scripts).
  • “scripts” , containing all the user defined scripts.

Obviously these paths are arbitrary and you can choose differently. Now let’s have a look at our beanshell scripts.

bagi_import.bsh
This is an utility script automatically included each time the engine gets called, so we should put here all the general purpose functions. Here is how it is now:



import org.asteriskjava.fastagi.AgiChannel;
import org.asteriskjava.fastagi.AgiException;
import org.asteriskjava.fastagi.AgiRequest;
import org.asteriskjava.fastagi.BaseAgiScript;

import org.beanizer.bagiserver.BAgiServer;

BAgiServer bagi;
AgiRequest request;
AgiChannel channel;

say(String string){
    java.rmi.server.UID uid=new java.rmi.server.UID();
    String command="echo '" + string + "' | text2wave -scale 8 -o "+ "/tmp/"+uid.toString() +".ulaw -otype ulaw -";     
    channel.exec("System",command );
    channel.streamFile("/tmp/" +uid.toString());
    channel.exec("System","rm /tmp/"+ uid.toString() +".ulaw" );
}
answer(){
    channel.answer();
}
hangup(){
    channel.hangup();
}
getDigits(int number){
    StringBuffer sb=new StringBuffer();
    for(int t=0;t<number;t++){
        sb.append(channel.waitForDigit(10000));
    }
    return sb.toString();
}

wait(int millis){
    (new Thread()).sleep(millis);
} 

Here we basically import some useful classes, instantiate BAgiServer,AgiRequest and AgiChannel, and then define some functions. These just do what their names say, note how most of the time we just wrap AgiChannel’s methods (“answer” and “hangup” i.e.).

“getDigits”, makes use of the method “waitForDigit” of AgiChannel to wait for the user to input a number of digits specified by the function parameter and returns the entire number as a string.

“say”, is used to achieve text-to-speech. Instead of using festival or flite, here we use a lighter approach. Using the “exec” method of AgiChannel we launch “text2wave” on the pbx host (so text2wave must be installed there), and save the resulting sound file in a tmp directory with a random name.

We then use “streamFile” to stream the sound to the user and finally delete the sound file from the tmp directory.


default.bsh
This is called when no script to be executed has been specified.


answer();
wait(1000);
say("No script defined");
wait(1000);
hangup();

As you can see, it makes use of the functions defined in bagi_import.bsh.  It answers the call, waits for a second, says a phrase, waits another second and then hangs up.

test.bsh
And now an example of a user defined script. This script must reside in the “scripts” directory, and the agi call from asterisk should be something like this:

exten => 1200,1,Agi(agi://localhost/server.agi?script=test)

And here is the code:

 


import org.asteriskjava.fastagi.command.*;
answer();
wait(1000);
say("Please, enter extension code");
String code = getDigits(3);
say("Trying to call extension:");
channel.sayDigits(code);
channel.exec("ChannelRedirect", channel.getName() + "|from-internal|" + code + "|1");
hangup();

I’m sure you can appreciate how easy it is now to write a script. In this case the script answers and waits for a while, then asks for an extension number to be dialed, reads aloud the dialed code and then tries to redirect the call to the requested extension, using th “ChannelRedirect” asterisk command. Then it hangs up.

If you want to test this approach, here are a couple of guidelines:

  • The directory structure  should be like this:

basedir
scriptlib
bagi_import.bsh
default.bsh
scripts
your_scripts_go_here.bsh
lib
asteriskjava.jar  (the name depends on the version)
beanshell.jar        (the name depends on the version)
bagiserver.jar     (it’s just org.beanizer.bagiserver.BAgiServer.class jarred)
fastagi-mapping.properties

  •         from “basedir”, start the  engine including in your classpath the “lib” dir and all the jars it contains, and launching the class org.asteriskjava.fastagi.DefaultAgiServer

 

Conclusions
The proposed is just a skeleton of what could grow as a more complete agi scripting engine for java (or even ruby,python,javascript,groovy) developers involved in integrating asterisk with other platforms.
It’s quite easy to extend the engine by adding methods in the java class or, better, by adding functions to bagi_import.bsh

LEAVE A COMMENT