Mod lua
From FreeSWITCH Wiki
Features
Write IVR scripts in Lua
It has a very easy to use syntax, see the Hello Lua script.
Serve configs (the same way mod_xml_curl does it)
Make API calls directly from Lua code
Lightweight
Stripped mod_lua.so is 272k
Highly Embeddable
As far as embeddability goes - Python ranks a 2, Perl ranks a 4, JavaScript is a 5, and Lua is a 10!
at the CLI : lua and luarun
You can issue a "luarun /path/to/script.lua" and launch a thread which your lua script will run in. The "lua" command is for inline lua like from the dialplan i.e. ${lua(codehere)}. "luarun" will spawn a thread while "lua" will block until the code is complete. A "~" in front of the argument will run a single line lua command. Note that lua scripts executed with luarun cannot write to the console through stream:write API as there is no stream.
Passing Arguments
Arguments are passed as space-separated values:
luarun arg1 arg2 arg3
Arguments are accessed with "argv" like this:
my_first_var = argv[1]; my_next_var = argv[2];
And so on...
freeswitch@DVORAK> lua ~print(string.find("1234#5678", "(%d+)#(%d+)"))
1 9 1234 5678
freeswitch@DVORAK> luarun ~print(string.find("1234#5678", "(%d+)#(%d+)"))
+OK
1 9 1234 5678
freeswitch@DVORAK> luarun ~stream:write("1234#5678")
+OK
2011-06-20 13:35:35.274782 [ERR] mod_lua.cpp:191 [string "line"]:1: attempt to index global 'stream' (a nil value)
stack traceback:
[string "line"]:1: in main chunk
freeswitch@DVORAK> lua ~stream:write("1234#5678")
1234#5678
Configuring
For IVR use
Nothing should be needed here.
For making API calls
api = freeswitch.API();
digits = api:execute("regex", "testing1234|/(\\d+)/|$1");
-- The returned output of the API call is stored in the variable if you need it.
freeswitch.consoleLog("info", "Extracted digits: " .. digits .. "\n")
Note: Please take care to escape the arguments that you pass, as has been done to the regex string above.
To call another Lua script
api = freeswitch.API();
reply = api:executeString("luarun another.lua");
For serving configuration
mod_lua allows you to replace request for configuration data from a lookup in the static XML to your script.
See Mod_lua/Serving_Configuration for more information.
Lua scripts at startup
Here is a minimum configuration file:
<configuration name="lua.conf" description="LUA Configuration">
<settings>
<!--
The following options identifies a lua script that is launched
at startup and may live forever in the background.
You can define multiple lines, one for each script you
need to run.
-->
<!--<param name="startup-script" value="startup_script_1.lua"/>-->
<!--<param name="startup-script" value="startup_script_2.lua"/>-->
</settings>
</configuration>
The start-up script values represent lua scripts (located inside the scripts/ directory) that are launched when FreeSWITCH is started. The scripts live in their own thread. You can use them to run simple tasks (and then let them finish) or looping forever, watching (for example) for events, generating calls or whatever you like.
Sample Dialplan
<action application="lua" data="helloworld.lua arg1 arg2"/>
NOTE: arguments can be accessed by using argv[1] argv[2] in your script
NOTE: for looking up the location of the helloworld.lua file, it looks in prefix/scripts by default
Sample IVR's
Hello Lua
-- answer the call
session:answer();
-- sleep a second
session:sleep(1000);
-- play a file
session:streamFile("/path/to/blah.wav");
-- hangup
session:hangup();
Lua Regex Example
using this method, you can execute regex conditions from inside your lua scripts. (thanks bkw_)
session:execute("set", "some_chan_variable=${regex(" .. destination .. "|^([0-9]{10})$)}")
if destination is a lua variable.. with your destination number in it... and your regex was ^([0-9]{10})$ the result will be put in "some_chan_variable" that you can get thorough a session:getVariable
you can also do
session:execute("set", "some_chan_variable=${regex(" .. destination .. "|^(0|61)([2,3,7,8][0-9]{8})$|$2)}")
to match and return parts of your regex.
Native Lua Pattern Matching
Lua supports a simple but powerful pattern matching syntax. It is less powerful than PCRE but it handles the majority of pattern matching cases will ever need in a simple script.
The following is a simple script that can be run with "luarun" from fs_cli and demonstrates the capturing of two values from a data string:
-- pattern.lua
data = "1234#5678";
_,_,var1,var2 = string.find(data,"(%d+)#(%d+)");
freeswitch.consoleLog("INFO","\ndata: " .. data .. "\nvar1: " .. var1 .. "\nvar2: " .. var2 .. "\n");
Output:
freeswitch@internal> luarun pattern.lua
+OK
2011-04-18 08:28:49.242080 [INFO] switch_cpp.cpp:1197
data: 1234#5678
var1: 1234
var2: 5678
See also:
- FreeSWITCH book pages 149-151
- http://www.lua.org/pil/20.2.html and http://www.lua.org/pil/20.3.html
- http://www.lua.org/manual/5.1/manual.html#5.4.1
Misc. Samples
Run a shell command
When using session:execute() and api:execute() to execute a shell command (ie: bash script) it will only return the error code integer.
To return the output of a shell command use io.popen(). The following example Lua function is from http://lua-users.org/wiki/ShellAccess.
function shell(c)
local o, h
h = assert(io.popen(c,"r"))
o = h:read("*all")
h:close()
return o
end
More Samples
- See scripts/lua directory on the file system
- Example IVRs
- Lua MythTV alert example
- Fakecall responder
- Fun Lua Examples
- Call retry based on hangup cause
- Bridging two calls with retry
FAQ
Where is my debug information?
Q: When I use static XML or xml_curl, I see the commands executed in the fs_cli and in the "application log" section of the xml_cdr file. However, when I run commands via my lua script, I don't see that information anywhere?
A: When you have a actual call session, you can use session:execute("$application","$data") just like in the static XML - it will then show up in the fs_cli and the xml_cdr application log. If you don't have a call session - e.g. if you are running lua as a background application or from the CLI, then you have to use other commands which may not be as easily logged.
Where do I put 3rd part Lua scripts/modules?
Q: Where do I need to stick my Lua classes for FreeSWITCH to see them. I am running a Lua script from within the /scripts folder, but it includes (requires) another Lua file. FreeSWITCH.
A: (short answer) /usr/local/share/lua/5.1/
Alternatively, you can install a system Lua (think apt-get or yum), and the Lua embedded in freeswitch will look in the add-ons directory.
To do this in the mod_lua.conf file (currently not possible), the following development would be needed.
* Install them to a nonstandard. * Push a nonstandard place into the path of where it looks.
NOTE: Assuming you have <param name="module-directory" value="$${base_dir}/scripts/?.so"/> in lua.conf.xml, in the scripts, require statements in the form of:
require "luasql.mysql"
This will look for the shared object ${base_dir}/scripts/luasql/mysql.so
NOTE: The native MySQL driver for Lua has a very bad memory leak. Do not use it.
How can I make it use the "system lua"
Q: I have Lua installed, but mod_lua seems to ignore the Lua binary.
A: Lua is so small that the whole ball of wax is statically linked into the module!
How can I get Lua to see my own libraries using "require"
Q: Can I use the require mechanism for including libraries with the Lua in FreeSWITCH?
A: You will need to alter the LUA_PATH variable to instruct the embedded Lua inside FreeSWITCH to find your libraries. A simple startup script to do this is:
#!/bin/bash export LUA_PATH=/usr/local/freeswitch/scripts/?.lua\;\; ulimit -s 240 screen /usr/local/freeswitch/bin/freeswitch
Can I access a database via ODBC?
Yes, there are two ways for doing this: freeswitch.Dbh and LuaSQL. The latter is described here.
With LuaSQL, you can query an ODBC data source, but also native drivers for PostgreSQL, Oracle or MySQL are available. The LuaSQL module itself will make the connection to your database. LuaSQL does not provide connection pooling as is possible with freeswitch.Dbh.
See LuaSQL (LuaSQL 2.1.1) for more information.
Here is a short howto for Installing LuaSQL.
WARNING: On x64_86 you need to change in config a few things
http://luaforge.net/frs/download.php/2686/luasql-2.1.1.tar.gz tar xfvz luasql-2.1.1.tar.gz cd luasql-2.1.1/
The file is divided into three broad sections: 1. DB Name 2. Paths to different libraries 3. Paths to DB related libraries
First check the #2 if it matches your flavor of OS.
Then, you have to select only *one* driver to compile at a time. Suppose you want to compile MySQL, then you have to uncomment only the MySQL line from #1 and #3. All other lines in both these sections should be commented.
NOTE: On RHEL / CentOS, the path for MySQL libs and include is not the same as the default config file that comes with LuaSQL. Please make the changes as follows:
DRIVER_LIBS= -L/usr/lib/mysql -lmysqlclient -lz DRIVER_INCS= -I/usr/include/mysql
Example: to use Microsoft SQL Server:
sed -i 's/#T= odbc/T= odbc/g' config sed -i 's/T=sqlite3/#T=sqlite3/g' config DRIVER_LIBS= -L/usr/lib64 -lodbc DRIVER_INCS= -DUNIXODBC -I/usr/include
Set proper values for LUA_LIBDIR, LUA_DIR , LUA_INC , DRIVER_LIBS, DRIVER_INCS
To allow compile properly:
sed -i 's/WARN= /WARN= -fPIC /g' config
Compile.
make make install
First you need to download and install Lua, and download LuaSQL, and compile and install whichever database modules you want to use. Next you can do something like:
#!/usr/local/bin/lua
require "luasql.mysql"
env = assert (luasql.mysql())
con = assert (env:connect("database","username","password","localhost"))
cur = assert (con:execute"SELECT * FROM table")
row = cur:fetch ({}, "a")
session:setVariable("varname", tostring(row.column));
cur:close()
con:close()
env:close()
For ODBC, you need to edit the odbc.ini and odbcinst.ini (both in /etc on RHEL). A typical MySQL configuration looks like:
File: odbcinst.ini # Driver from the MyODBC package # Setup from the unixODBC package [MySQL] Description = ODBC for MySQL Driver = /usr/lib/libmyodbc.so Setup = /usr/lib/libodbcmyS.so FileUsage = 1
Note that the *.so files should be in the folder.
File: odbc.ini [mydsn] Driver = MySQL SERVER = localhost PORT = 3306 DATABASE = mydatabase OPTION = 67108864 SOCKET = /var/lib/mysql/mysql.sock
NOTE:I found you also need to set some environment variables for scripts to run properly
echo export LUA_PATH=/usr/local/freeswitch/scripts/?.lua >> /etc/bashrc echo export LUA_CPATH=/usr/local/freeswitch/scripts/?.so >> /etc/bashrc echo export PATH=$PATH:/usr/local/freeswitch/bin >> ~/.bashrc
NOTE: you need to symlink the shared object (i.e., mysql.so) to /usr/local/lib/lua/5.1/luasql/mysql.so
WARNING: This change breaks load of external Lua modules http://fisheye.freeswitch.org:8081/changelog/FreeSWITCH?cs=9605
NOTE: Fixed http://fisheye.freeswitch.org:8081/changelog/FreeSWITCH?cs=10306
How can I find useful undocumented Session Functions?
There may be times when a function gets added, but not documented. This simple Lua script may help.
-- This function simply tells us what function are available in Session
-- It just prints a list of all functions. We may be able to find functions
-- that have not yet been documented but are useful. I did :)
function printSessionFunctions( session )
metatbl = getmetatable(session)
if not metatbl then return nil end
local f=metatbl['.fn'] -- gets the functions table
if not f then return nil end
print("\n***Session Functions***\n")
for k,v in pairs(f) do print(k,v) end
print("\n\n")
end
new_session = freeswitch.Session() -- create a blank session
printSessionFunctions(new_session)
At freeswitch revision 16256 this is the output of the script:
process_callback_result function: 0x79b380 flushDigits function: 0x798db0 execute function: 0x798ed0 setAutoHangup function: 0x798de0 collectDigits function: 0x798c30 mediaReady function: 0x7993d0 speak function: 0x79b2d0 setHangupHook function: 0x799510 getDigits function: 0x798c60 check_hangup_hook function: 0x799380 playAndGetDigits function: 0x798cf0 getState function: 0x79b1e0 hangupState function: 0x79b4e0 say function: 0x79b150 recordFile function: 0x79b210 waitForAnswer function: 0x798ea0 sleep function: 0x798d50 setEventData function: 0x798f30 setPrivate function: 0x79b540 setLUA function: 0x7995d0 begin_allow_threads function: 0x799320 setInputCallback function: 0x7994e0 unsetInputCallback function: 0x799470 run_dtmf_callback function: 0x799400 get_cb_args function: 0x799020 read function: 0x798cc0 getPrivate function: 0x79b570 answer function: 0x79b450 setVariable function: 0x79b510 getXMLCDR function: 0x798f60 originate function: 0x799570 destroy function: 0x7992f0 answered function: 0x798e70 set_tts_parms function: 0x79b300 sendEvent function: 0x798f00 ready function: 0x799540 preAnswer function: 0x79b4b0 streamFile function: 0x798d20 hangupCause function: 0x79b1b0 setDTMFCallback function: 0x79b2a0 getVariable function: 0x79b350 transfer function: 0x798c90 get_uuid function: 0x798ff0 hangup function: 0x79b480 end_allow_threads function: 0x799350 flushEvents function: 0x798d80 sayPhrase function: 0x79b180
API
Events
These methods apply to generating events.
event:addBody
--Create Custom event
custom_msg = "dial_record_id: " .. dial_record_id .. "\n" ..
"call_disposition: " .. Disposition .. "\n" ..
"campaign_number: " .. Campaign .. "\n" ..
"called_number: " .. dial_num .."\n" ;
local e = freeswitch.Event("custom", "dial::dial-result");
e:addBody(custom_msg);
e:fire();
You can add as much data to the body as you like, in this case 4 items are to be sent.
The result will be
[Content-Length] => 555
[Content-Type] => text/event-plain
[Body] => Array
(
[Event-Subclass] => dial::dial-result
[Event-Name] => CUSTOM
[Core-UUID] => 2dc7cc50-b157-4868-ae16-04e5f4b95dae
[FreeSWITCH-Hostname] => pp6.noble.co.uk
[FreeSWITCH-IPv4] => 192.168.0.106
[FreeSWITCH-IPv6] => ::1
[Event-Date-Local] => 2009-02-17 23:15:49
[Event-Date-GMT] => Tue, 17 Feb 2009 23:15:49 GMT
[Event-Date-Timestamp] => 1234912549610060
[Event-Calling-File] => switch_cpp.cpp
[Event-Calling-Function] => fire
[Event-Calling-Line-Number] => 297
[Content-Length] => 85
[Body] => Array
(
[dial_record_id] => 1234
[call_disposition] => AA
[campaign_number] => 20
[called_number] => 7777777
)
)
)
event:addHeader
event:delHeader
event:fire
bkw gave the example of such...
local event = freeswitch.Event("message_waiting");
event:addHeader("MWI-Messages-Waiting", "no");
event:addHeader("MWI-Message-Account", "sip:1000@10.0.1.100");
event:addHeader("Sofia-Profile", "internal");
event:fire();
event:getBody
event:getHeader
This is a generic API call.
event:getHeader("Caller-Caller-ID-Name")
Or, This can be used inside of a dialplan.lua to get certain information
params:getHeader("variable_sip_req_uri")
event:getType
event:serialize
Use this to dump all available Headers to the console.
-- Print as text
io.write(params:serialize());
io.write(params:serialize("text"));
-- Print as JSON
io.write(params:serialize("json"));
Or this to display them as an info message.
freeswitch.consoleLog("info",params:serialize())
event:setPriority
Sending an Event
Using luarun to execute this code you can toggle the MWI on a registered phone on and off.
local event = freeswitch.Event("message_waiting");
event:addHeader("MWI-Messages-Waiting", "no");
event:addHeader("MWI-Message-Account", "sip:1002@10.0.1.100");
event:fire();
Sessions
The following methods can be applied to existing sessions.
session:answer
Answer the session:
session:answer();
session:answered
- checks whether the session is flagged as answered (true anytime after the call has been answered)
session:bridged
Check to see if this session's channel is bridged to another channel.
if (session:bridged() == true) do
-- Do something
end
session:check_hangup_hook
session:collectDigits
session:destroy
Destroys the session and releases resources. This is done for you when your script ends, but if your script contains an infinite loop you can use this to terminate the session.
session:execute
session:execute(app, data)
local mySound = "/usr/local/freeswitch/sounds/music/16000/partita-no-3-in-e-major-bwv-1006-1-preludio.wav"
session:execute("playback", mySound)
Callbacks (DTMF and friends) CAN NOT EXECUTE during an execute.
session:flushDigits
session:flushEvents
session:get_uuid
session:getDigits
Get digits:
- getDigits has three arguments: max_digits, terminators, timeout
- max_digits: maximum number of DTMF tones that will be collected
- terminators: buffer of characters that will terminate the digit collection
- timeout: maximum time in milliseconds allowed for digit collection
- return: buffer containing collected digits
- The method blocks until one of the exit criteria is met.
digits = session:getDigits(5, "#", 3000);
freeswitch.consoleLog("info", "Got dtmf: ".. digits .."\n");
session:getState
Get the call state, i.e. "CS_EXECUTE". The call states are described in "switch_types.h".
state=session:getState();
session:getVariable
To get system variables such as ${hold_music}
local moh = session:getVariable("hold_music")
--[[ events obtained from "switch_channel.c"
regards Monroy from Mexico
]]
session:getVariable("context");
session:getVariable("destination_number");
session:getVariable("caller_id_name");
session:getVariable("caller_id_number");
session:getVariable("network_addr");
session:getVariable("ani");
session:getVariable("aniii");
session:getVariable("rdnis");
session:getVariable("source");
session:getVariable("chan_name");
session:getVariable("uuid");
session:hangup
You can hang up a session and provide an optional Hangup causes.
session:hangup("USER_BUSY");
or
session:hangup(); -- default normal_clearing
session:hangupCause
You can find the hangup cause of an answered call and/or the reason an originated call did not complete. See Hangup causes.
-- Initiate an outbound call
obSession = freeswitch.Session("sofia/192.168.0.4/1002")
-- Check to see if the call was answered
if obSession:ready() then
-- Do something good here
else -- This means the call was not answered ... Check for the reason
local obCause = obSession:hangupCause()
freeswitch.consoleLog("info", "obSession:hangupCause() = " .. obCause )
if ( obCause == "USER_BUSY" ) then -- SIP 486
-- For BUSY you may reschedule the call for later
elseif ( obCause == "NO_ANSWER" ) then
-- Call them back in an hour
elseif ( obCause == "ORIGINATOR_CANCEL" ) then -- SIP 487
-- May need to check for network congestion or problems
else
-- Log these issues
end
end
session:hangupState
session:insertFile
session:insertFile(<orig_file>, <file_to_insert>, <insertion_sample_point>)
Inserts one file into another. All three arguments are required. The third argument is in samples, and is the number of samples into orig_file that you want to insert file_to_insert. The resulting file will be written at the sample rate of the session, and will replace orig_file.
Because the position is given in samples, you'll need to know the sample rate of the file to properly calculate how many samples are X seconds into the file. For example, to insert a file two seconds into a .wav file that has a sample rate of 8000Hz, you would use 16000 for the insertion_sample_point argument.
Note that this method requires an active channel with a valid session object, as it needs the sample rate and the codec info from the session.
Examples:
On a ulaw channel, insert bar.wav one second into foo.wav:
session:insertFile("foo.wav", "bar.wav", 8000)
Prepend bar.wav to foo.wav:
session:insertFile("foo.wav", "bar.wav", 0)
Append bar.wav to foo.wav:
session:insertFile("bar.wav", "foo.wav", 0)
session:mediaReady
session:originate
This construct is deprecated use:
new_session = freeswitch.Session("sofia/gateway/gatewayname/18001234567", session);
The code below is here for the sake of history only; please do not use it going forward.
local new_session = freeswitch.Session(); originate deprecated see below new_session.originate(session, dest[, timeout]);
dest - quoted dialplan destanation. For example: "sofia/internal/1000@10.0.0.1" or "sofia/gateway/my_sip_provider/my_dest_number"
timeout - origination timeout in seconds
Note: in recent git versions, session.originate expects 4 arguments; the example above does not work (anymore?).
session:playAndGetDigits
Plays a file and collects DTMF digits. Digits are matched against a regular expression. Non-matching digits or a timeout can trigger the playing of an audio file containing an error message. Optional arguments allow you to transfer to an extension on failure, and store the entered digits into a channel variable.
Syntax
digits = session:playAndGetDigits (
min_digits, max_digits, max_attempts, timeout, terminators,
prompt_audio_files, input_error_audio_files,
digit_regex, variable_name, digit_timeout,
transfer_on_failure)
Arguments
min_digits The minimum number of digits required. max_digits The maximum number of digits allowed. max_attempts The number of times this function waits for digits and replays the prompt_audio_file when digits do not arrive. timeout The time (in milliseconds) to wait for a digit. terminators A string containing a list of digits that cause this function to terminate. prompt_audio_file The initial audio file to play. Playback stops if digits arrive while playing. This file is replayed after each timeout, up to max_attempts. input_error_audio_file The audio file to play when a digit not matching the digit_regex is received. Received digits are discarded while this file plays. Specify an empty string if this feature is not used. digit_regex The regular expression used to validate received digits. variable_name (Optional) The channel variable used to store the received digits. digit_timeout (Optional) The inter-digit timeout (in milliseconds). When provided, resets the timeout clock after each digit is entered, thus giving users with limited mobility the ability to slowly enter digits without causing a timeout. If not specified, digit_timeout is set to timeout. transfer_on_failure (Optional) In the event of a failure, this function will transfer the session to an extension in the dialplan. The syntax is "extension-name [dialplan-id [context]]".
Discussion
- This function returns an empty string when all timeouts and retry counts are exhausted.
- When the maximum number of allowable digits is reached, the function returns immediately, even if a terminator was not entered.
- If the user forgets to press one of the terminators, but has made a correct entry, the digits are returned after the next timeout.
- The session has to be answered before any digits can be processed. If you do not answer the call you, the audio will still play, but no digits will be collected.
Examples
Example 1:
digits = session:playAndGetDigits(2, 5, 3, 3000, "#", "/prompt.wav", "/error.wav", "\\d+") freeswitch.consoleLog("info", "Got DTMF digits: ".. digits .."\n")This example causes freeswitch to play prompt.wav and listen for between 2 and 5 digits, ending with the # key. If the user enters nothing (or something other than a digit, like the * key) error.wav is played, and the process is repeated another two times.
Example 2:
digits = session:playAndGetDigits(1, 1, 3, 3000, "", "/menu.wav", "/error.wav", "[134]", 3, "digits_received", "operator XML default") freeswitch.consoleLog("info", "Got DTMF digits: ".. digits .."\n")This time, we require only one digit, and it must be 1, 3 or 4. If the user does not comply after three attempts, they are transferred to the operator extension in the "default" XML dial plan.
If the user presses a correct key, that digit is returned to the caller, and the "digits_received" channel variable is set to the same value.
Reminder: If you need to match the * key in the regular expression, you will have to quote it twice:
digits = session:playAndGetDigits(2, 5, 3, 3000, "#", "/sr8k.wav", "", "\\d+|\\*");
session:preAnswer
Pre answer the session:
session:preAnswer();
session:read
Play a file and get digits.
digits = session:read(5, 10, "/sr8k.wav", 3000, "#");
freeswitch.consoleLog("info", "Got dtmf: ".. digits .."\n");
session:read has 5 arguments: <min digits> <max digits> <file to play> <inter-digit timeout> <terminators>
session:ready
- checks whether the session is still active (true anytime between call starts and hangup)
- also session:ready will return false if the call is being transferred. Bottom line is you should always be checking session:ready on any loops and periodicly throughout your script and exit asap if it returns false.
See #session:hangupCause for more detail on if NOT ready.
while (session:ready() == true) do -- do something here end
session:recordFile
syntax is session:recordFile(file_name, max_len_secs, silence_threshold, silence_secs)
silence_secs is the amount of silence to tolerate before ending the recording.
Example:
function grabaEntrada(tablaGraba, tablaGrabaMod)
local razon, OWNER, destino = '', '', '';
local handle, k, v = nil, nil, nil
OWNER = 'grabaEntrada'
function mkDir(dir)
local d = nil
local OWNER = 'mkDir'
local tmp = ''
if type(dir) == 'string' then
dir = backSlash(dir)
for d in string.gfind(dir, ".-/") do
tmp = tmp .. d
end
dir = tmp
os.execute(string.format('MKDIR %s', string.gsub(dir, '/', '%\\') ))
end
end
function onInputCBF(s, _type, obj, arg)
local k, v = nil, nil
if tablaGrabaMod._debug then
for k, v in pairs(tablaGrabaMod) do
print(string.format('tablaGrabaMod k-> %s v->%s\n', tostring(k), tostring(v)))
end
for k, v in pairs(obj) do
printSessionFunctions(obj)
print(string.format('obj k-> %s v->%s\n', tostring(k), tostring(v)))
end
if _type == 'table' then
for k, v in pairs(_type) do
print(string.format('_type k-> %s v->%s\n', tostring(k), tostring(v)))
end
end
print(string.format('\n(%s == dtmf) and (obj.digit [%s] == [%s] tablaGrabaMod.dtmf)\n', _type, obj.digit, tablaGrabaMod.dtmf))
end
if (_type == "dtmf") and (obj.digit == tablaGrabaMod.dtmf) then print(string.format('\nBREAK!!!\n'))
return 'break'
else print(string.format('\nCONTINUE!!!\n'))
return ''
end
end
if type(tablaGraba) ~= 'table' then
razon= string.format('ERROR Parametros invalidos')
return false
end
if type(tablaGrabaMod) ~= 'table' then
tablaGrabaMod = {
segundosMax = 7,
umbralSilencio = 500,
segundosInterrupcion = 3,
dtmf = '#',
_debug = false
}
end
if type(tablaGraba.dir ) ~= 'string'then
razon= string.format('ERROR tablaGraba.dir no es tipo string, Abortando')
return false
end
if type(tablaGraba.audioT) ~= 'table' then
razon= string.format('ERROR tablaGraba.audioT no es tipo table, Abortando')
return false
end
if #tablaGraba.audioT == 0 then
razon= string.format('WARNING tablaGraba.audioT NO tiene audios definidos, defina audios (tablaGraba.audioT = {\'aviso.wav\', \'beep.wav\' })')
end
if (type(tablaGraba.nombre) ~= 'string' ) or (tablaGraba.nombre == '') then
tablaGraba.nombre = dameNombreUnico(CUUID)..'.wav'
razon= string.format('WARNING tablaGraba.nombre NO definido e esta VACIO, valor Asignado [%s]', tablaGraba.nombre)
end
if type(tablaGrabaMod.dtmf) ~= 'string' or #tablaGrabaMod.dtmf == 0 then
tablaGrabaMod.dtmf = '#'
end
if type(tablaGrabaMod._debug) ~= 'boolean' then
tablaGrabaMod._debug = false
end
tablaGrabaMod.segundosMax = tonumber(tablaGrabaMod.segundosMax) or 7;
tablaGrabaMod.umbralSilencio = tonumber(tablaGrabaMod.umbralSilencio) or 500;
tablaGrabaMod.segundosInterrupcion = tonumber(tablaGrabaMod.segundosInterrupcion) or 3;
tablaGrabaMod.dtmf = tablaGrabaMod.dtmf:sub(1,1) --Solo un caracter
if tablaGraba.dir:sub(#tablaGraba.dir) ~= '/' then
tablaGraba.dir = tablaGraba.dir..'/'
end
destino = string.format('%s%s', tablaGraba.dir, tablaGraba.nombre)
handle = io.open(destino, 'w')
if handle == nil then --Trata de crear el directorio sino existe
local tmp, d, dir = '', '', ''
dir = destino
for d in string.gfind(dir, ".-/") do
tmp = tmp .. d
end
dir = tmp
mkDir(dir
else
handle:close()
end
session:execute('flush_dtmf')
razon= string.format('LISTO path [%s] FLUSH_DTMF EJECUTADO!!', destino)
parametros = creaInforme(razon, string.format('INFO %s', razon), CONSOLE_W, OWNER)
if session:ready() then
for k, v in pairs(tablaGraba.audioT) do
session:streamFile(v)
end
session:setInputCallback('onInputCBF', '');
session:recordFile(destino, tablaGrabaMod.segundosMax, tablaGrabaMod.umbralSilencio, tablaGrabaMod.segundosInterrupcion);
end
end
Regards Arturo Monroy
session:sayPhrase
Play a speech phrase macro.
session.sayPhrase(macro_name [,macro_data] [,language]);
- macro_name - (string) The name of the say macro to speak.
- macro_data - (string) Optional. Data to pass to the say macro.
- language - (string) Optional. Language to speak macro in (ie. "en" or "fr"). Defaults to "en".
To capture events or DTMF, use it in combination with session:setInputCallback
Example:
function key_press(session, input_type, data, args)
if input_type == "dtmf" then
freeswitch.consoleLog("info", "Key pressed: " .. data["digit"])
return "break"
end
end
if session:ready() then
session:answer()
session:execute("sleep", "1000")
session:setInputCallback("key_press", "")
session:sayPhrase("voicemail_menu", "1:2:3:#", "en")
end
When used with setInputCallback, the return values and meanings are as follows:
- true or "true" - Causes prompts to continue speaking.
- Any other string value interrupts the prompt.
session:sendEvent
session:setAutoHangup
By default, lua script hangs up when it is done executing. If you need to run the next action in your dialplan after the lua script, you will need to setAutoHangup to false. The default value is true.
session:setAutoHangup(false)
session:setHangupHook
In your lua code, you can use setHangupHook to define the function to call when the session hangs up.
function myHangupHook(s, status, arg)
freeswitch.consoleLog("NOTICE", "myHangupHook: " .. status .. "\n")
-- close db_conn and terminate
db_conn:close()
error()
end
blah="w00t";
session:setHangupHook("myHangupHook", "blah")
Other possibilities to exit the script (and throwing an error)
return "exit";
or
return "die";
or
s:destroy("error message");
session:setInputCallback
function my_cb(s, type, obj, arg)
if (arg) then
io.write("type: " .. type .. "\n" .. "arg: " .. arg .. "\n");
else
io.write("type: " .. type .. "\n");
end
if (type == "dtmf") then
io.write("digit: [" .. obj['digit'] .. "]\nduration: [" .. obj['duration'] .. "]\n");
else
io.write(obj:serialize("xml"));
e = freeswitch.Event("message");
e:add_body("you said " .. obj:get_body());
session:sendEvent(e);
end
end
blah="w00t";
session:answer();
session:setInputCallback("my_cb", "blah");
session:streamFile("/tmp/swimp.raw");
session:setVariable
Set a variable on a session:
session:setVariable("varname", "varval");
session:sleep
session:sleep(3000);
- This will allow callbacks to DTMF to occur and session:execute("sleep", "5000"), won't.
session:speak
session:set_tts_parms("flite", "kal");
session:speak("Please say the name of the person you're trying to contact");
session:say
Plays pre-recorded sound files for things like numbers, dates, currency, etc. Refer to Misc. Dialplan Tools say for info about the say application.
Arguments: <data><lang><say_type><say_method>
Example:
session:say("12345", "en", "number", "pronounced");
session:streamFile
Stream a file endless to the session
session:streamFile("/tmp/blah.wav");
Stream a file endless to the session starting at sample_count?
session:streamFile("/tmp/blah.wav", <sample_count>);
session:transfer
Transfer the current session. The arguments are extensions, dialplan and context.
session:transfer("3000", "XML", "default");
execution of your lua script will immediately stop, make sure you set session:setAutoHangup(false) if you don't want your call to disconnect
session:unsetInputCallback
session:unsetInputCallback()
session:waitForAnswer
Non-Session API
These methods are generic in that they do not apply to a session or an event. For example, printing data to the FreeSWITCH console is neither event- nor session-specific.
freeswitch.API
api = freeswitch.API(); -- get current time in milliseconds time = api:getTime()
freeswitch.bridge
session1 = freeswitch.Session("sofia/internal/1001%192.168.1.1");
session2 = freeswitch.Session("sofia/internal/1002%192.168.1.1");
freeswitch.bridge(session1, session2);
freeswitch.consoleCleanLog
freeswitch.consoleCleanLog("This Rocks!!!\n");
freeswitch.consoleLog
Log something to the freeswitch logger. Arguments are loglevel, message.
freeswitch.consoleLog("info", "lua rocks\n");
freeswitch.consoleLog("notice", "lua rocks\n");
freeswitch.consoleLog("err", "lua rocks\n");
freeswitch.consoleLog("debug", "lua rocks\n");
freeswitch.consoleLog("warning","lua rocks\n");
freeswitch.Dbh
Get an ODBC or core (sqlite) database handle from FreeSWITCH and perform an SQL query through it.
Advantage of this method is that it makes use of connection pooling provided by FreeSWITCH which gives a nice increase in speed when compared to creating a new TCP connection for each LuaSQL env:connect().
It works as follows:
local dbh = freeswitch.Dbh("dsn","user","pass") -- when using ODBC
-- OR --
local dbh = freeswitch.Dbh("core:my_db") -- when using sqlite
assert(dbh:connected()) -- exits the script if we didn't connect properly
dbh:test_reactive("SELECT * FROM my_table",
"DROP TABLE my_table",
"CREATE TABLE my_table (id INTEGER(8), name VARCHAR(255))")
dbh:query("INSERT INTO my_table VALUES(1, 'foo')") -- populate the table
dbh:query("INSERT INTO my_table VALUES(2, 'bar')") -- with some test data
dbh:query("SELECT id, name FROM my_table", function(row)
stream:write(string.format("%5s : %s\n", row.id, row.name))
end)
dbh:query("UPDATE my_table SET name = 'changed'")
stream:write("Affected rows: " .. dbh:affected_rows() .. "\n")
dbh:release() -- optional
- freeswitch.Dbh("dsn", "user", "pass") gets an ODBC db handle from the pool.
- freeswitch.Dbh("core:dbname") gets a core db (sqlite) db handle from the pool (this automatically creates the db if it didn't exist yet).
- dbh:connected() checks if the handle is still connected to the database, returns true if connected, false otherwise.
- dbh:test_reactive("test_sql", "drop_sql", "reactive_sql") performs test_sql and if it fails performs drop_sql and reactive_sql (handy for initial table creation purposes)
- dbh:query("query", function()) takes the query as a string and an optional Lua callback function that is called on each row returned by the db.
- The callback function is passed a table representation of the current row for each iteration of the loop.
Syntax of each row is: { ["column_name_1"] = "value_1", ["column_name_2"] = "value_2" }. - If you (optionally) return a number other than 0 from the callback-function, you'll break the loop.
- The callback function is passed a table representation of the current row for each iteration of the loop.
- dbh:affected_rows() returns the number of rows affected by the last run INSERT, DELETE or UPDATE on the handle. It does not respond to SELECT operations.
- dbh:release() (optional) releases the handle back to the pool so it can be re-used by another thread. This is also automatically done when the dbh goes out of scope and is garbage collected (for example when your script returns).
Take a look here for some examples.
freeswitch.email
Send an email with optional (converted) attachment.
Note that for this to work you have to have an MTA installed on your server, you also need to have 'mailer-app' configured in your switch.conf.xml.
You can use freeswitch.email as follows:
freeswitch.email(to, from, headers, body, file, convert_cmd, convert_ext)
- to (mandatory) a valid email address
- from (mandatory) a valid email address
- headers (mandatory) for example "subject: you've got mail!\n"
- body (optional) your regular mail body
- file (optional) a file to attach to your mail
- convert_cmd (optional) convert file to a different format before sending
- convert_ext (optional) to replace the file's extension
For example:
freeswitch.email("receiver@bar.com",
"sender@foo.com",
"subject: Voicemail from 1234\n",
"Hello,\n\nYou've got a voicemail, click the attachment to listen to it.",
"message.wav",
"mp3enc",
"mp3")
then the following system command will be executed before attaching "message.mp3" to the mail and send it on its way:
mp3enc message.wav message.mp3
freeswitch.Event
This is firing a custom event my::event.
local event = freeswitch.Event("custom", "my::event");
event:addHeader("My-Header", "test");
event:fire();
--Another one
function FSMan:fire(nombreHeader, header, body)--Manda un evento MESSAGE a algun receptor
local miEvento = freeswitch.Event("MESSAGE");
nombreHeader = Utils:trim(nombreHeader); header = Utils:trim(header); body = Utils:trim(body);
if (nombreHeader == false ) then nombreHeader="Nombre_Header_Generico" end
if (header == false) then header="Header_Generico" end
if (body == false) then body="Body_Generico" end
miEvento:addHeader(nombreHeader, header);
miEvento:addBody(body);
miEvento:fire();
end
freeswitch.EventConsumer
Consumes events from FreeSWITCH.
Usage:
con = freeswitch.EventConsumer("<event_name>"[,"<subclass type>"]);
-- pop() returns an event or nil if no events
con:pop()
-- pop(1) blocks until there is an event
con:pop(1)
-- pop(1,500) blocks for max half a second until there is an event
con:pop(1,500)
Examples:
con = freeswitch.EventConsumer("all");
session = freeswitch.Session("sofia/default/dest@host.com");
while session:ready() do
session:execute("sleep", "1000");
for e in (function() return con:pop() end) do
print("event\n" .. e:serialize("xml"));
end
end
-- or
while session:ready() do
for e in (function() return con:pop(1,1000) end) do
print("event\n" .. e:serialize("xml"))
end
end
-- You may subscribe to specific events if you want to, and even subclasses
con = freeswitch.EventConsumer("CUSTOM");
con = freeswitch.EventConsumer("CUSTOM","conference::maintenance");
-- wait for a specific event but continue after 500 ms
function poll()
-- create event and listener
local event = freeswitch.Event("CUSTOM", "ping::running?")
local con = freeswitch.EventConsumer("CUSTOM", "ping::running!")
-- add text ad libitum
event:addHeader("hi", "there")
-- fire event
event:fire()
-- and wait for reply but not very long
if con:pop(1, 500) then
print("reply received")
return true
end
print("no reply")
return false
end
freeswitch.getGlobalVariable
Retrieves a global variable
my_globalvar = freeswitch.getGlobalVariable("varname")
freeswitch.IVRMenu
hash = {
["main"] = undef,
["name"] = "top",
["greet_long"] = "phrase:demo_ivr_main_menu",
["greet_short"] = "phrase:demo_ivr_main_menu_short",
["invalid_sound"] = "ivr/ivr-that_was_an_invalid_entry.wav",
["exit_sound"] = "voicemail/vm-goodbye.wav",
["confirm_macro"] = "undef",
["confirm_key"] = "undef",
["confirm_attempts"] = "3",
["inter_digit_timeout"] = "2000",
["digit_len"] = "1",
["timeout"] = "10000",
["max_failures"] = "3"
}
top = freeswitch.IVRMenu(hash["main"],
hash["name"],
hash["greet_long"],
hash["greet_short"],
hash["invalid_sound"],
hash["exit_sound"],
hash["confirm_macro"],
hash["confirm_key"],
hash["confirm_attempts"],
hash["inter_digit_timeout"],
hash["digit_len"],
hash["timeout"],
hash["max_failures"]);
top:bindAction("menu-exec-app", "playback /tmp/swimp.raw", "2");
top:execute(session, "top");
When using SAY, 3 additional variables have to be set or you will get the following error:
> [ERR] mod_lua.cpp:182 Error in IVRMenu expected 16..16 args, got 13 stack traceback:
> [C]: in function 'IVRMenu'
> /usr/local/freeswitch/scripts/ivr.lua:19: in main chunk
These variables are:
["tts_engine"] = "flite",
["tts_voice"] = "rms",
["max_timeouts"] = "2"
freeswitch.msleep
Tells script to sleep for a specified number of milliseconds.
NOTE: Do not use this on a session-based script or bad things will happen.
-- Sleep for 500 milliseconds freeswitch.msleep(500);
freeswitch.Session
Create a new session.
local session = freeswitch.Session("sofia/10.0.1.100/1001");
session:transfer("3000", "XML", "default");
Create a new session with execute_on_answer variable set.
local session = freeswitch.Session("[execute_on_answer=info notice]sofia/10.0.1.100/1001");
stream:write
API commands
You can write FreeSWITCH API commands *in Lua* by using the lua FreeSWITCH API command to run a script and pass the arguments in, then whatever you write with the stream object is what you get as a reply to that command. For example, given a script in the scripts directory called hello.lua with the following content:
stream:write("hello world")
Running 'lua hello.lua' from the FreeSWITCH console would return back "hello world".
Or calling it from the dialplan like so:
<action application="set" data="foo=${lua(hello.lua)}"/>
Would set the channel variable "foo" to "hello world".
Web page interaction (via mod_xml_rpc)
--
-- lua/api.lua
--
-- enable mod_xml_rpc and try http://127.0.0.1:8080/api/lua?lua/api.lua in your webbrowser
--
stream:write("Content-Type: text/html\n\n");
stream:write("<title>FreeSWITCH Command Portal</title>");
stream:write("<h2>FreeSWITCH Command Portal</h2>");
stream:write("<form method=post><input name=command size=40> ");
stream:write("<input type=submit value=\"Execute\">");
stream:write("</form><hr noshade size=1><br>");
command = env:getHeader("command");
if (command) then
api = freeswitch.API();
reply = api:executeString(command);
if (reply) then
stream:write("<br><B>Command Result</b><br>" .. reply .. "\n");
end
end
env:addHeader("cool", "true");
stream:write(env:serialize() .. "\n\n");
Example: Call Control
-- cc.lua
-- call control lua script
--
dialA = "sofia/gateway/fs1/9903"
dialB = "user/1001"
legA = freeswitch.Session(dialA)
dispoA = "None"
while(legA:ready() and dispoA ~= "ANSWER") do
dispoA = legA:getVariable("endpoint_disposition")
freeswitch.consoleLog("INFO","Leg A disposition is '" .. dispoA .. "'\n")
os.execute("sleep 1")
end -- legA while
if( not legA:ready() ) then
-- oops, lost leg A handle this case
freeswitch.consoleLog("NOTICE","It appears that " .. dialA .. " disconnected...\n")
else
legB = freeswitch.Session(dialB)
dispoB = "None"
while(legA:ready() and legB:ready() and dispoB ~= "ANSWER") do
if ( not legA:ready() ) then
-- oops, leg A hung up or got disconnected, handle this case
freeswitch.consoleLog("NOTICE","It appears that " .. dialA .. " disconnected...\n")
else
os.execute("sleep 1")
dispoB = legB:getVariable("endpoint_disposition")
freeswitch.consoleLog("NOTICE","Leg B disposition is '" .. dispoB .. "'\n")
end -- inner if legA ready
end -- legB while
if ( legA:ready() and legB:ready() ) then
freeswitch.bridge(legA,legB)
else
-- oops, one of the above legs hung up, handle this case
freeswitch.consoleLog("NOTICE","It appears that " .. dialA .. " or " .. dialB .. " disconnected...\n")
end
end -- outter if legA ready
Special Case: env object
When lua is called as the hangup hook there will be a special env object that contains all the channel variables from the channel that just disconnected.
Add an extension to test this feature:
<extension name="lua-env-hangup-hook-test">
<condition field="destination_number" expression="^(1234)$">
<action application="answer"/>
<action application="set" data="api_hangup_hook=lua hook-test.lua"/>
<action application="set" data="my_custom_var=foobar"/>
<action application="sleep" data="10000"/>
<action application="hangup"/>
</condition>
</extension>
Then add freeswitch/scripts/hook-test.lua:
-- hook-test.lua
-- demonstrates using env to look at channel variables in hangup hook script
-- See everything
dat = env:serialize()
freeswitch.consoleLog("INFO","Here's everything:\n" .. dat .. "\n")
-- Grab a specific channel variable
dat = env:getHeader("uuid")
freeswitch.consoleLog("INFO","Inside hangup hook, uuid is: " .. dat .. "\n")
-- Drop some info into a log file...
res = os.execute("echo " .. dat .. " >> /tmp/fax.log")
res = os.execute("echo YOUR COMMAND HERE >> /tmp/fax.log")
-- If you created a custom variable you can get it also...
dat = env:getHeader("my_custom_var")
freeswitch.consoleLog("INFO","my_custom_var is '" .. dat .. "'\n")
Watch the FS console and dial 1234 and then hangup. You'll see all your channel variables!

