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