Potential paging system replacement

Intro

Due to limitations of the current paging server in sipXecs there have been discussions on how to replace it with a FreeSWITCH based system. I've posted rough code and basic instructions on how to implement this proof of concept paging system.

Some advantages of this setup over the existing paging system:

  • Two-way paging, meaning those being paged can talk back to the pager
    • WARNING: this can cause nasty feedback if phones are too close together
  • End users can enable a limited "do not disturb" so they are not paged. This is limited to 99 minutes.
  • Can scale to much more extensions than the current paging system since FreeSWITCH conference is used.

TO-DO:

  • Write this in a sane language with proper ESL support. Lua + PHP + fs_ivrd isn't really suited for tasks this big.
  • Integrate with sipXconfig, etc.
  • Multi-tenant/multi-server
  • Call-Info field for compatibility with snom, others
  • Probably a lot more

Setup

THIS GUIDE ASSUMES YOU ARE INSTALLING ON A FRESH CentOS 5.5 x86_64 SERVER WITHOUT sipXecs ON IT AND THAT YOU'VE ALREADY BUILT FreeSWITCH FROM SOURCE

WAV files used in this example are attached

To start fs_ivrd you'll need to use this init script. Place it in /etc/init.d and mark it executable.

I'm not going to go through the particulars of setting up FreeSWITCH or installing the supporting database. I will give as much detail as possible.

You'll need to install the epel YUM repo. Install it by following the instructions here: http://fedoraproject.org/wiki/EPEL

Once you've got that repo set up you'll need to install lua and some supporting libraries, as well as postgresql

yum install lua lua-sql-postgresql postgresql postgresql-server

After those packages are installed you'll need to add a line to /usr/local/freeswitch/conf/autoload_configs/lua.conf.xml

<param name="module-directory" value="/usr/lib64/lua/5.1/?.so"/>

This is so the built in FreeSWITCH lua interpreter can utilize the postgresql library

Now, you need to execute the following SQL to create the database and table that the paging system utilizes:

CREATE DATABASE conftest;
CREATE TABLE page_data (entry_id integer NOT NULL, page_group character varying(255) NOT NULL, sip_uri character varying(255) NOT NULL, page_timeout timestamp without time zone NOT NULL);
CREATE USER conftest WITH PASSWORD 'password123';
GRANT ALL PRIVILEGES ON DATABASE conftest to conftest;

Here is an example of the data that should be entered in this database:

entry_id

page_group

sip_uri

page_timeout

1

3402

7001@sipx.domain.tld

2011-05-10 14:19:30

2

3402

7002@sipx.domain.tld

2011-05-10 14:19:30

3

3402

7003@sipx.domain.tld

2011-05-10 14:19:30

Now you need to allow conftest to log in to the database. To do this you will need to set a password on the posgresql user:

ALTER USER postgres WITH PASSWORD 'password123';

Now edit /var/lib/pgsql/data/pg_hba.conf to be exactly like:

local all all password
host all all 127.0.0.1/32 password
host    all         all         ::1/128               ident sameuser
local all conftest crypt

Now restart postgresql

FreeSWITCH

Create a new conference profile called intercom in 8/usr/local/freeswitch/conference.conf.xml* and set it to the following:

    <profile name="intercom">
      <param name="domain" value="$${domain}"/>
      <param name="rate" value="16000"/>
      <param name="interval" value="20"/>
      <param name="energy-level" value="300"/>
      <param name="sound-prefix" value="$${sounds_dir}/en/us/callie"/>
      <param name="muted-sound" value="conference/conf-muted.wav"/>
      <param name="unmuted-sound" value="conference/conf-unmuted.wav"/>
      <param name="alone-sound" value="conference/conf-alone.wav"/>
      <param name="moh-sound" value="$${hold_music}"/>
      <param name="enter-sound" value=""/>
      <param name="exit-sound" value=""/>
      <param name="kicked-sound" value=""/>
      <param name="locked-sound" value="conference/conf-locked.wav"/>
      <param name="is-locked-sound" value="conference/conf-is-locked.wav"/>
      <param name="is-unlocked-sound" value="conference/conf-is-unlocked.wav"/>
      <param name="pin-sound" value="conference/conf-pin.wav"/>
      <param name="bad-pin-sound" value="conference/conf-bad-pin.wav"/>
      <param name="caller-id-name" value="$${outbound_caller_name}"/>
      <param name="caller-id-number" value="$${outbound_caller_id}"/>
      <param name="comfort-noise" value="true"/>
    </profile>

Be sure to create a gateway pointing to you FreeSWITCH server in sipXecs and point the appropriate numbers to it

Add the following to your FreeSWITCH dialpan (change expressions to meet your needs):

   <extension name="Page Test">
     <condition field="destination_number" expression="^(3402)$">
      <action application="lua" data="/usr/local/freeswitch/scripts/page_outcall.lua $1"/>
      <action application="set" data="ivr_path=/usr/local/freeswitch/scripts/page_int.php"/>
      <action application="socket" data="127.0.0.1:9090 async full"/>
     </condition>
   </extension>

   <extension name="outbound-socket">
      <condition field="destination_number" expression="^(3403)$">
      <action application="lua" data="/usr/local/freeswitch/scripts/page_time_set.lua"/>
    </condition>
   </extension>

now reload FreeSWITCH

Add the following scripts to your /usr/local/freeswitch/scripts folder:

page_int.php
#!/usr/bin/php -q

<?php

// set a couple of things so we dont kill the system
ob_implicit_flush(true);
set_time_limit(30);

// Open stdin so we can read the data in
$in = fopen("php://stdin", "r");

// Connect to conference

echo "sendmsg\n";
echo "call-command: execute\n";
echo "execute-app-name: set\n";
echo "execute-app-arg: conference_auto_outcall_flags=mute\n\n";

echo "sendmsg\n";
echo "call-command: execute\n";
echo "execute-app-name: set\n";
echo "execute-app-arg: api_hangup_hook=conference $1 kick all\n\n";

echo "sendmsg\n";
echo "call-command: execute\n";
echo "execute-app-name: conference\n";
echo "execute-app-arg: $1@intercom\n\n";

echo "sendmsg\n";
echo "call-command: execute\n";
echo "execute-app-name: sleep\n";
echo "execute-app-arg: 350\n";
echo "event-lock:true\n\n";

// Play a prompt at the beginning of the page/conference
echo "sendmsg\n";
echo "call-command: execute\n";
echo "execute-app-name: set\n";
echo "execute-app-arg: tmp=\${conference $1 play /usr/local/freeswitch/sounds/tones/norstar.wav}\n\n";

fclose($in);

?>
page_outcall.lua
require "luasql.postgres"

-- Exit if no argument
if argv[1] == nil then
	print ("One argument is required")
	os.exit(0)
end

-- Get current epoch
today = os.time()
-- Connect to DB, get page info
env = assert (luasql.postgres())
con = assert (env:connect("conftest","conftest","password123","localhost"))
cur = assert (con:execute("SELECT entry_id, page_group, sip_uri, extract(epoch FROM page_timeout) FROM page_data WHERE page_group = " .. argv[1]))
row = cur:fetch ({}, "a")
page_table = {}
i = 1

-- iterate through list of extensions to be paged and discard those on timeout
while row do
	if tonumber(row.date_part) > today then
		print ("Skipping")
        -- keeps system from paging the pager :-P
        elseif row.sip_uri == sipuri then
                print ("Skipping")
	else
		page_table[i] = row.sip_uri
	end
	row = cur:fetch(row, "a")
	i = i + 1
end

-- Close DB connection as we won't be needing it anymore
cur:close()
con:close()
env:close()

session:answer()
cidname = session:getVariable("caller_id_name")
session:execute("export", "sip_invite_params=intercom=true")
session:execute("export", "sip_auto_answer=true")
session:execute("set", "conference_auto_outcall_caller_id_name=Page From " .. cidname)
session:execute("set", "conference_auto_outcall_caller_id_number=" .. argv[1])
session:execute("set", "conference_auto_outcall_timeout=60")

-- Make the calls
for i,v in pairs (page_table) do
	session:execute("conference_set_auto_outcall", "{alert_info=sipXpage}sofia/custom_dialplan/" .. v .. ";sipx-noroute=VoiceMail;sipx-userforward=false+flags")
end
page_time_set.lua
require "luasql.postgres"

-- Get caller's SIP URI
sipuri = session:getVariable("sip_from_uri")

-- Connect to DB
env = assert (luasql.postgres())
con = assert (env:connect("conftest","conftest","password123","localhost"))
cur = assert (con:execute("SELECT sip_uri FROM page_data WHERE sip_uri = '" .. sipuri .. "'"))
row = cur:fetch ({}, "a")
uri_table = {}
i = 1
while row do
	uri_table[i] = row.sip_uri
	row = cur:fetch(row, "a")
	i = i + 1
end
-- WAKE UP! :-)
session:answer()
session:sleep(1000)
-- If user isn't in DB then they don't need to set a timeout, now do they?
if uri_table[1] == nil then
	session:streamFile("/usr/local/freeswitch/sounds/en/us/callie/voicemail/16000/vm-that_was_an_invalid_ext.wav")
else
        -- Get amount of minutes user wants to be on timeout for
	digits = session:playAndGetDigits(1, 2, 1, 3000, "#", "/usr/local/freeswitch/sounds/tones/enter-minutes.wav", "/usr/local/freeswitch/sounds/en/us/callie/voicemail/16000/vm-abort.wav", ".+")
end
-- Get current epoch
today = os.time()
-- If user doesn't enter anything then why continue?
if tonumber(digits) == nil then
	session:hangup()
-- Update DB, split out at 20 to make file playback easier
elseif tonumber(digits) < 20 then
	new_time = today + (tonumber(digits) * 60)
	session:streamFile("/usr/local/freeswitch/sounds/en/us/callie/digits/16000/" .. digits .. ".wav")
	session:streamFile("/usr/local/freeswitch/sounds/en/us/callie/time/16000/minutes.wav")
	res = assert (con:execute("UPDATE page_data SET page_timeout=to_timestamp(" .. new_time .. ") WHERE sip_uri = '" .. sipuri .. "'"))
else
	new_time = today + (tonumber(digits) * 60)
	digsplit = {}
	for dig in digits:gmatch("%d") do table.insert(digsplit, dig) end
	session:streamFile("/usr/local/freeswitch/sounds/en/us/callie/digits/16000/" .. digsplit[1] .. "0.wav")
	session:streamFile("/usr/local/freeswitch/sounds/en/us/callie/digits/16000/" .. digsplit[2] .. ".wav")
	session:streamFile("/usr/local/freeswitch/sounds/en/us/callie/time/16000/minutes.wav")
	res = assert (con:execute("UPDATE page_data SET page_timeout=to_timestamp(" .. new_time .. ") WHERE sip_uri = '" .. sipuri .. "'"))
end
session:sleep(500)
session:hangup()
-- Kill DB
cur:close()
con:close()
env:close()