Hi,

it's raining, so seems like its good time for a second part of this small DSM tutorial.

Stefan Sayer wrote:
David J. wrote:
 Hi Stefan,

I am trying to accomplish a script that parallel forks to many callers and then the one who enters the right DTMF code gets the call.

In order to do this, I am guessing, I need b2b_mode which sends RINGING back to the Caller, does the forking plays a WAV/MP3 announcement to the Callee's and some sort of DTMF detection that captures the dialed digits and verify's them via script, database or whatever.

actually, in order to be able to prompt the callees and collect the DTMF from them, you will have to establish separate calls to them - so that b2b mode is not really suitable. Once the right callee leg is identified, though, you can connect audio from the caller and the callee leg by joining the same conference room. You can interact between the two legs (e.g. when one hangs up) by sending events back and forth, the only thing you need is to know in both legs is the id of the other leg (local-tag).

But, to start from the beginning. We will need two DSM scripts, one for the caller, one for the callee leg. We call that application quizconnect, and we tell dsm to load DSM application configurations by setting in dsm.conf:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
conf_dir=/usr/local/etc/sems/dsm/
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Then we can create /usr/local/etc/sems/dsm/quizconnect.conf which loads the scripts and sets the settings for the quizconnect application:
/usr/local/etc/sems/dsm/quizconnect.conf:
-----------------------------------
diag_path=/usr/local/lib/sems/dsm/quizconnect/
load_diags=quizconnect_caller,quizconnect_callee
register_apps=quizconnect_caller,quizconnect_callee
mod_path=/usr/local/lib/sems/dsm/
preload_mods=mod_mysql
run_invite_event=yes
set_param_variables=yes
#run_system_dsms=

# some configuration parameters
#  - can be used with e.g. $config.prompt_path
prompt_path=/usr/local/lib/sems/dsm/quizconnect/prompts/
-----------------------------------

We have preloaded the mysql module, which needs this to be initialized and read its configuration (which contains the DB connection, for example). We also set run_invite_event=yes in the dsm config, that way we get an 'invite' event into the DSM scripts. Now we create two scripts, /usr/local/lib/sems/dsm/quizconnect/quizconnect_caller.dsm and /usr/local/lib/sems/dsm/quizconnect/quizconnect_callee.dsm .

When that invite event comes, we tell dsm to not connect the session (i.e. reply with 200 and connect audio), but to reply 183 (early media) and play a file:

/usr/local/lib/sems/dsm/quizconnect/quizconnect_caller.dsm:
-------------------------------------------------------
import(mod_dlg);
initial state START;
transition "got INVITE in caller leg" START - invite -> RUN_INVITE;

apparently no-one tried this, because here we obviously have a c&p typo:


- > initial state RUN_INVITE enter {
+ > state RUN_INVITE enter {

we should only have one initial state ('START').

  log(2, "got invite!");
  set($connect_session=0);
  -- reply with 183 and parse SDP
  dlg.acceptInvite(183, Session Progress);
  -- set input and output of the session (we have $connect_session=0)
  setInOutPlaylist();
  -- play some welcome message
  sets($prompt_name=$(config.prompt_path)/welcome_caller.wav)
  playFile($prompt_name);
};
-------------------------------------------------------

To run this, in sems.conf set application=quizconnect_caller .

so, that's the first part. in the next part, we will see how we can read callee numbers from mysql DB, create some callee legs, and interact between caller and callee legs.

now, first we want to handle the error that the file does not exist or can not be opened. For this, we create a special transition, an "exception transition". Once an exception is thrown (by some internal function or a module function), the current sequence of statements is interrupted, and only exception transitions are executed; all other transitions are ignored.

quizconnect_caller.dsm:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
transition "error opening file" RUN_INVITE - exception / {
  dlg.reply(500, Server Internal Error);
  stop(false);
} -> END;

state END;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

using 'stop', we stop execution of this session. stop() or stop(true) sends a BYE, which we don't want here, and stop(false) just ends the session after the current event is processed.

Now, a little improvement in the development environment is helpful: when we update the DSM script, we don't want to have to restart SEMS every time. So, what we do is we load the xmlrpc2di module

sems.conf:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
load_plugins=wav;session_timer;uac_auth;dsm;monitoring;xmlrpc2di
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

and we tell xmlrpc2di to export the functions from dsm module directly

xmlrpc2di.conf:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
export_di=yes
direct_export=dsm;monitoring
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

then we can in another shell write that few python lines to reload the quizconnect config:
$ python
Python 2.6.4 (r264:75706, Dec  7 2009, 18:45:15)
[GCC 4.4.1] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from xmlrpclib import *
>>> s = ServerProxy('http://localhost:8090')
>>> s.calls()
0
>>> s.loadConfig('/usr/local/etc/sems/dsm/quizconnect.conf', 'quizconnect')
[200, 'OK']

this way we can replace existing applications (the new set of scripts are executed for new calls only), and also load other new applications into the running server.

So, every time we change the DSM scripts, we can simply run s.loadConfig(...).

The next task should be to get the list of possible destinations (callees) from the database; we will use a mysql DB. For this, we create a table in mysql

CREATE TABLE callees (
  id int(10) unsigned NOT NULL auto_increment,
  caller varchar(128) NOT NULL,
  callee varchar(128) NOT NULL,
  pin varchar(32) NOT NULL,
  PRIMARY KEY  (id)
);

and insert some rows:
insert into callees (caller,callee,pin) values ("35","john","12345");
insert into callees (caller,callee,pin) values ("35","anna","54321");

so, if the number '35' will be called, john and anna will be connected, and john should enter 12345, while anna should enter 54321.

In the script we use the mod_mysql module:

quizconnect_caller.dsm:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
import(mod_mysql);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

we can set the DB connection in our quizconnect.conf (or pass it to mysql.connect() action):

quizconnect.conf:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
db_url=mysql://user:p...@localhost/quizconnect
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

btw, as we have seen above with prompt_path, all configuration keys from our quizconnect.conf are accessible with $config.key, so we could also write mysql.connect($config.db_url).

With the beginning of processing the call, we connect to the database.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
state RUN_INVITE enter {
  log(2, "got invite!");
  set($connect_session=0);
  myslq.connect();
  -- reply with 183 and parse SDP
  dlg.acceptInvite(183, Session Progress);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

mysql.connect doesn't throw an exception if the connection fails ('Access denied', 'server has gone away'), instead it sets an error code ($errno) which is the old style of reporting errors. To throw an exception in that case, we can use throwOnError() and also handle that exception:

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
state RUN_INVITE enter {
  log(2, "got invite!");
  set($connect_session=0);
  mysql.connect();
  throwOnError()
  -- reply with 183 and parse SDP
  dlg.acceptInvite(183, Session Progress);
  -- set input and output of the session (we have $connect_session=0)
  setInOutPlaylist();
  -- play some welcome message
  sets($prompt_name=$(config.prompt_path)/welcome_caller.wav)
  playFile($prompt_name);
};

transition "error opening file" RUN_INVITE - exception; test(#type==file) / {
  log(0, "error opening file!");
  dlg.reply(500, Server Internal Error);
  stop(false);
} -> END;

transition "DB error" RUN_INVITE - exception; test(#type==connection) / {
  log(0, "error connecting to DB!");
  logParams(0);
  dlg.reply(500, Server Internal Error);
  stop(false);
} -> END;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

When an exception is processed, the parameters (#paramname) are those of the exception - thus if we do logParams(0), we can see the actual error from DB in the log.

Now we can select the callees from the database:

  set($query_k...@user);
  mysql.query(select callee, pin from callees where caller=$query_key);

which should give us $errno and $db.rows.

We will now apply a small trick: We want to process the results of the DB query, and make some transitions depending on whether that worked or not. By doing a "repost()", the current event is evaluated once more. so we can do:

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
...
  set($query_k...@user);
  mysql.query(select callee, pin from callees where caller=$query_key);
  repost();
};

transition "query failed" RUN_INVITE - test($errno!="") / {
  log(1, "query failed!");
  logParams(0);
  dlg.reply(500, Server Internal Error);
  stop(false);
} -> END;

transition "no results" RUN_INVITE - test($db.rows==0) / {
  log(3, "no results");
  dlg.reply(404, Not found);
  stop(false);
} -> END;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

to handle query error and empty destination set. If we have some results, we go to a new state CREATE_CALLEE_LEGS:

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
transition "we have results" RUN_INVITE - test($db.rows!=0) / set($callee_counter=0) -> CREATE_CALLEE_LEGS;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

We will actually loop a few times into that state, for every row that we get from the database - thus we do repost() every time we enter the state, to make sure we don't stay there (its a 'transitional state'):


~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
state CREATE_CALLEE_LEGS
 enter {
   repost();
 };

transition "create one more" CREATE_CALLEE_LEGS - test($callee_counter<$db.rows) / {
   -- this will fill $callee, $pin from current row
   mysql.getResult($callee_counter);

   set(b_leg_caller=quizconnect);
   set(b_leg_callee=$callee);
   set(b_leg_domain=sip.domain.net);
   set(b_leg_app=quizconnect_callee);

   -- pass $pin to other leg
   set(b_leg_var.pin=$pin);

   dlg.dialout(b_leg);

   -- if that worked, we have the ID of the other leg in $b_leg_ltag
   log(3, $b_leg_ltag);

   inc(callee_counter);
} -> CREATE_CALLEE_LEGS;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

We use the dlg.dialout function to create an entirely new call, which will execute the quizconnect_callee application, and will be from quizconnect to [email protected]. We also pass the pin to the other leg, this variable can be accessed as $pin in the other script.

When all callee legs are created, we go to a new state. Here we also handle the BYE in the caller leg, at least we stop our own call:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
transition "done creating callee legs" CREATE_CALLEE_LEGS - test($callee_counter==$db.rows) -> WAIT_CALLEE;

state WAIT_CALLEE;

transition "BYE received" WAIT_CALLEE - hangup / stop -> END;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

So, that's for the second part of that tutorial. In the third part, we are hopefully finally going to see how to interact between the call legs, and how to connect the legs into the same conference. Some hints:
- postEvent() can post events with variables between DSM call legs
- mod_conference is used to join audio of two calls to a conference

Attached is the full script that we have so far. I hope you have some fun trying it out, and I would be happy about some feedback.

Best Regards
Stefan

--
Stefan Sayer
VoIP Services Consulting and Development

Warschauer Str. 24
10243 Berlin

tel:+491621366449
sip:[email protected]
email/xmpp:[email protected]


import(mod_dlg);
import(mod_mysql);

initial state START;
transition "got INVITE in caller leg" START - invite -> RUN_INVITE;

state RUN_INVITE enter {
  log(2, "got invite!");
  set($connect_session=0);
  mysql.connect();
  throwOnError()
  -- reply with 183 and parse SDP
  dlg.acceptInvite(183, Session Progress);
  -- set input and output of the session (we have $connect_session=0)
  setInOutPlaylist();
  -- play some welcome message
  sets($prompt_name=$(config.prompt_path)/welcome_caller.wav)
  playFile($prompt_name);

  set($query_k...@user);
  mysql.query(select callee, pin from callees where caller=$query_key);
  repost();
};

transition "error opening file" RUN_INVITE - exception; test(#type==file) / {
  log(0, "error opening file!");
  dlg.reply(500, Server Internal Error);
  stop(false);
} -> END;

transition "DB error when connecting" RUN_INVITE - exception; 
test(#type==connection) / {
  log(0, "error connecting to DB!");
  logParams(0);
  dlg.reply(500, Server Internal Error);
  stop(false);
} -> END;

transition "query failed" RUN_INVITE - test($errno!="") / {
  log(1, "query failed!");
  logParams(0);
  dlg.reply(500, Server Internal Error);
  stop(false);
} -> END;

transition "no results" RUN_INVITE - test($db.rows==0) / {
  log(3, "no results");
  dlg.reply(404, Not found);
  stop(false);
} -> END;

transition "we have results" RUN_INVITE - test($db.rows!=0) / 
set($callee_counter=0) -> CREATE_CALLEE_LEGS;

state CREATE_CALLEE_LEGS
 enter {
   repost();
 };

transition "create one more" CREATE_CALLEE_LEGS - 
test($callee_counter<$db.rows) / {
   -- this will fill $callee, $pin from current row
   mysql.getResult($callee_counter);
   
   set(b_leg_caller=quizconnect);
   set(b_leg_callee=$callee);
   set(b_leg_domain=sip.outbound.domain.net);
   set(b_leg_app=quizconnect_callee);

   -- pass $pin to other leg
   set(b_leg_var.pin=$pin);

   dlg.dialout(b_leg);

   -- if that worked, we have the ID of the other leg in $b_leg_ltag
   log(3, $b_leg_ltag);

   inc(callee_counter);
} -> CREATE_CALLEE_LEGS;

transition "done creating callee legs" CREATE_CALLEE_LEGS - 
test($callee_counter==$db.rows) -> WAIT_CALLEE;

state WAIT_CALLEE;

transition "BYE received" WAIT_CALLEE - hangup / stop -> END;

state END;
_______________________________________________
Sems mailing list
[email protected]
http://lists.iptel.org/mailman/listinfo/sems

Reply via email to