summaryrefslogtreecommitdiff
path: root/misc
diff options
context:
space:
mode:
authorStefan Wintermeyer <stefan.wintermeyer@amooma.de>2012-12-17 12:01:45 +0100
committerStefan Wintermeyer <stefan.wintermeyer@amooma.de>2012-12-17 12:01:45 +0100
commitb80bd744ad873f6fc43018bc4bfb90677de167bd (patch)
tree072c4b0e33d442528555b82c415f5e7a1712b2b0 /misc
parent3e706c2025ecc5523e81ad649639ef2ff75e7bac (diff)
Start of GS5.
Diffstat (limited to 'misc')
-rw-r--r--misc/TODO-Liste.txt0
-rw-r--r--misc/etc/cron.d/logout_phones3
-rw-r--r--misc/etc/ssl/amooma/server.pem36
-rw-r--r--misc/etc/ssl/amooma/server_cert.pem22
-rw-r--r--misc/etc/ssl/amooma/server_key.pem16
-rw-r--r--misc/example/apache-gs5.conf24
-rw-r--r--misc/example/nginx73
-rw-r--r--misc/example/xml-interface.txt25
-rw-r--r--misc/freeswitch/conf/freeswitch.xml759
-rw-r--r--misc/freeswitch/scripts/acd_wait.lua45
-rw-r--r--misc/freeswitch/scripts/common/call_forwarding.lua47
-rw-r--r--misc/freeswitch/scripts/common/call_history.lua140
-rw-r--r--misc/freeswitch/scripts/common/conference.lua239
-rw-r--r--misc/freeswitch/scripts/common/configuration_file.lua70
-rw-r--r--misc/freeswitch/scripts/common/database.lua151
-rw-r--r--misc/freeswitch/scripts/common/fapi.lua80
-rw-r--r--misc/freeswitch/scripts/common/ipcalc.lua27
-rw-r--r--misc/freeswitch/scripts/common/log.lua69
-rw-r--r--misc/freeswitch/scripts/common/node.lua73
-rw-r--r--misc/freeswitch/scripts/common/phone_number.lua359
-rw-r--r--misc/freeswitch/scripts/common/routing_tables.lua66
-rw-r--r--misc/freeswitch/scripts/common/sip_account.lua137
-rw-r--r--misc/freeswitch/scripts/common/str.lua136
-rw-r--r--misc/freeswitch/scripts/common/sync_log.lua39
-rw-r--r--misc/freeswitch/scripts/configuration.lua229
-rw-r--r--misc/freeswitch/scripts/configuration/freeswitch_xml.lua307
-rw-r--r--misc/freeswitch/scripts/configuration/sip.lua37
-rw-r--r--misc/freeswitch/scripts/dialplan/access_authorizations.lua52
-rw-r--r--misc/freeswitch/scripts/dialplan/acd.lua484
-rw-r--r--misc/freeswitch/scripts/dialplan/callthrough.lua148
-rw-r--r--misc/freeswitch/scripts/dialplan/cdr.lua71
-rw-r--r--misc/freeswitch/scripts/dialplan/dialplan.lua996
-rw-r--r--misc/freeswitch/scripts/dialplan/fax.lua232
-rw-r--r--misc/freeswitch/scripts/dialplan/functions.lua839
-rw-r--r--misc/freeswitch/scripts/dialplan/geo_number.lua89
-rw-r--r--misc/freeswitch/scripts/dialplan/hunt_group.lua202
-rw-r--r--misc/freeswitch/scripts/dialplan/phone_book.lua63
-rw-r--r--misc/freeswitch/scripts/dialplan/presence.lua84
-rw-r--r--misc/freeswitch/scripts/dialplan/route.lua265
-rw-r--r--misc/freeswitch/scripts/dialplan/session.lua224
-rw-r--r--misc/freeswitch/scripts/dialplan/sip_call.lua266
-rw-r--r--misc/freeswitch/scripts/dialplan/tenant.lua51
-rw-r--r--misc/freeswitch/scripts/dialplan/user.lua91
-rw-r--r--misc/freeswitch/scripts/dialplan/voicemail.lua155
-rw-r--r--misc/freeswitch/scripts/dialplan_default.lua64
-rw-r--r--misc/freeswitch/scripts/event/call_history_save.lua74
-rw-r--r--misc/freeswitch/scripts/event/cdr_save.lua105
-rw-r--r--misc/freeswitch/scripts/event/event.lua109
-rw-r--r--misc/freeswitch/scripts/event/perimeter.lua106
-rw-r--r--misc/freeswitch/scripts/event/presence_update.lua199
-rw-r--r--misc/freeswitch/scripts/event_manager.lua39
-rw-r--r--misc/freeswitch/scripts/fax_daemon.lua42
-rw-r--r--misc/freeswitch/scripts/ini/conferences.ini27
-rw-r--r--misc/freeswitch/scripts/ini/database.ini11
-rw-r--r--misc/freeswitch/scripts/ini/dialplan.ini11
-rw-r--r--misc/freeswitch/scripts/ini/events.ini8
-rw-r--r--misc/freeswitch/scripts/ini/gateways.ini.example23
-rw-r--r--misc/freeswitch/scripts/ini/perimeter.ini9
-rw-r--r--misc/freeswitch/scripts/ini/routes.ini77
-rw-r--r--misc/freeswitch/scripts/ini/sip_accounts.ini10
-rw-r--r--misc/freeswitch/scripts/ini/sofia.ini55
-rw-r--r--misc/freeswitch/scripts/phones/phone.lua114
-rw-r--r--misc/freeswitch/scripts/phones/siemens.lua45
-rw-r--r--misc/freeswitch/scripts/phones/snom.lua65
-rw-r--r--misc/freeswitch/scripts/phones/uacsta.lua100
-rw-r--r--misc/freeswitch/scripts/send_fax.lua170
-rw-r--r--misc/mon_ami/asterisk.py299
-rw-r--r--misc/mon_ami/freeswitch.py194
-rw-r--r--misc/mon_ami/helper.py21
-rw-r--r--misc/mon_ami/log.py71
-rwxr-xr-xmisc/mon_ami/mon-ami58
-rwxr-xr-xmisc/mon_ami/mon_ami10
-rw-r--r--misc/mon_ami/mon_ami_handler.py434
-rw-r--r--misc/mon_ami/mon_ami_main.py117
-rw-r--r--misc/mon_ami/mon_ami_server.py85
-rw-r--r--misc/mon_ami/sqliter.py132
-rw-r--r--misc/mon_ami/tcp_server.py39
-rw-r--r--misc/nginx/nginx.conf81
78 files changed, 10525 insertions, 0 deletions
diff --git a/misc/TODO-Liste.txt b/misc/TODO-Liste.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/misc/TODO-Liste.txt
diff --git a/misc/etc/cron.d/logout_phones b/misc/etc/cron.d/logout_phones
new file mode 100644
index 0000000..86b0ffd
--- /dev/null
+++ b/misc/etc/cron.d/logout_phones
@@ -0,0 +1,3 @@
+# Logout tagged phones
+23 1 * * * root /opt/GS5/script/logout_phones.sh
+
diff --git a/misc/etc/ssl/amooma/server.pem b/misc/etc/ssl/amooma/server.pem
new file mode 100644
index 0000000..d34e8db
--- /dev/null
+++ b/misc/etc/ssl/amooma/server.pem
@@ -0,0 +1,36 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXQIBAAKBgQDC9G3px4ew18PHIm8HJ3yXc9rxqM5uSn1qhjdoWM0zC0Qcue9k
+V+5ZUq356yjBbs5wXi3SEWfucXxhnwnzeIqvMeO6y0BiUVsClbqziRCho/hgHPTJ
+tZzjt6Mpl3D/9yeFKbah5lJ5qNm0T00ybpcSXC7w2Xv9ckji1DDtGo62fQIDAQAB
+AoGAHM0jl9AEednGcJrjsDjjLTTOebkolh6nHJ+re9zyo8HcVCob9cUPz15pmWxm
+Xv1RvkQLnOc5ZX6ak4l9XNzIEAvQXNRFXwCOyfpffx/8QhfrG0v2G+K2QG52VxQj
+tqnRdLf8HEhCmrJCMvMEAuQkAiirIMTFcaaP1CBbCilr8nkCQQDnSYxEfXMYi4iq
+9Xjwn8Ayh1koXFUY+5/0u9SGqzTeTxW1QN2hGhehsd0vlv4cJppcuL4Z+2VYqLQc
+zXDZo/MLAkEA18kSHLp+HCd1BW/JEoIQqWlTw6SRx+IsUN7UmnSZS4C+UPRbtq5I
+nzgzonZufOEmzoMdwbe9EHAl087f0UfxlwJBAKNIBhGYKvgqEdr3n2Dotuw1J1la
+De2sPpmtPPWxyoojdOTYHV8Np59MjSV6yHyhOBq7heGb3EmCGF25H7FWkE8CQGUN
+aakAgPxoUfn4zp4XQPxFMhAF6qtDtOMuZzvp7LwaD4ZT2PtlBOdjZ3LmqXlb61N8
+vZuxkx22l1BoqhIU8gMCQQDJsQr5y8UWamYZrNv5YRNnm8aGgJ83Gx3n5b8bKqjh
+TM8hqJfFTIfHr90GhHyok1aVkjF+sUtydX1R85IHTDz5
+-----END RSA PRIVATE KEY-----
+-----BEGIN CERTIFICATE-----
+MIIDfjCCAuegAwIBAgIJAKG1XaHFZ4gEMA0GCSqGSIb3DQEBBQUAMIGHMQswCQYD
+VQQGEwJERTEYMBYGA1UECBMPUmhlaW5sYW5kLVBmYWx6MRAwDgYDVQQHEwdOZXV3
+aWVkMRQwEgYDVQQKEwtBTU9PTUEgR21iSDEXMBUGA1UEAxMOR2VtZWluc2NoYWZ0
+IDQxHTAbBgkqhkiG9w0BCQEWDmluZm9AYW1vb21hLmRlMB4XDTExMDcyMjE0NDAy
+OFoXDTIxMDcxOTE0NDAyOFowgYcxCzAJBgNVBAYTAkRFMRgwFgYDVQQIEw9SaGVp
+bmxhbmQtUGZhbHoxEDAOBgNVBAcTB05ldXdpZWQxFDASBgNVBAoTC0FNT09NQSBH
+bWJIMRcwFQYDVQQDEw5HZW1laW5zY2hhZnQgNDEdMBsGCSqGSIb3DQEJARYOaW5m
+b0BhbW9vbWEuZGUwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAML0benHh7DX
+w8cibwcnfJdz2vGozm5KfWqGN2hYzTMLRBy572RX7llSrfnrKMFuznBeLdIRZ+5x
+fGGfCfN4iq8x47rLQGJRWwKVurOJEKGj+GAc9Mm1nOO3oymXcP/3J4UptqHmUnmo
+2bRPTTJulxJcLvDZe/1ySOLUMO0ajrZ9AgMBAAGjge8wgewwHQYDVR0OBBYEFATl
+qWtGbyeBIN9mR/4GV9jO7ON7MIG8BgNVHSMEgbQwgbGAFATlqWtGbyeBIN9mR/4G
+V9jO7ON7oYGNpIGKMIGHMQswCQYDVQQGEwJERTEYMBYGA1UECBMPUmhlaW5sYW5k
+LVBmYWx6MRAwDgYDVQQHEwdOZXV3aWVkMRQwEgYDVQQKEwtBTU9PTUEgR21iSDEX
+MBUGA1UEAxMOR2VtZWluc2NoYWZ0IDQxHTAbBgkqhkiG9w0BCQEWDmluZm9AYW1v
+b21hLmRlggkAobVdocVniAQwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOB
+gQCBIgWoP8YiP6tm8rhb81k6myP4ONO4vOaUz9bsadHDWNCTjiQxvo4uVYqTMLKa
+Bc7S0VpyvSg7/eGsSWxIUwdn6dUPdo51juGnnJ9dK9DuiNPHb0HP3UJo1gCWgs1v
+EnVGfKDdu9FfdNcQtIb28UfF8Pw8WA6mmhQOOh0M9d3ayQ==
+-----END CERTIFICATE-----
diff --git a/misc/etc/ssl/amooma/server_cert.pem b/misc/etc/ssl/amooma/server_cert.pem
new file mode 100644
index 0000000..021ad28
--- /dev/null
+++ b/misc/etc/ssl/amooma/server_cert.pem
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDfjCCAuegAwIBAgIJAKG1XaHFZ4gEMA0GCSqGSIb3DQEBBQUAMIGHMQswCQYD
+VQQGEwJERTEYMBYGA1UECBMPUmhlaW5sYW5kLVBmYWx6MRAwDgYDVQQHEwdOZXV3
+aWVkMRQwEgYDVQQKEwtBTU9PTUEgR21iSDEXMBUGA1UEAxMOR2VtZWluc2NoYWZ0
+IDQxHTAbBgkqhkiG9w0BCQEWDmluZm9AYW1vb21hLmRlMB4XDTExMDcyMjE0NDAy
+OFoXDTIxMDcxOTE0NDAyOFowgYcxCzAJBgNVBAYTAkRFMRgwFgYDVQQIEw9SaGVp
+bmxhbmQtUGZhbHoxEDAOBgNVBAcTB05ldXdpZWQxFDASBgNVBAoTC0FNT09NQSBH
+bWJIMRcwFQYDVQQDEw5HZW1laW5zY2hhZnQgNDEdMBsGCSqGSIb3DQEJARYOaW5m
+b0BhbW9vbWEuZGUwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAML0benHh7DX
+w8cibwcnfJdz2vGozm5KfWqGN2hYzTMLRBy572RX7llSrfnrKMFuznBeLdIRZ+5x
+fGGfCfN4iq8x47rLQGJRWwKVurOJEKGj+GAc9Mm1nOO3oymXcP/3J4UptqHmUnmo
+2bRPTTJulxJcLvDZe/1ySOLUMO0ajrZ9AgMBAAGjge8wgewwHQYDVR0OBBYEFATl
+qWtGbyeBIN9mR/4GV9jO7ON7MIG8BgNVHSMEgbQwgbGAFATlqWtGbyeBIN9mR/4G
+V9jO7ON7oYGNpIGKMIGHMQswCQYDVQQGEwJERTEYMBYGA1UECBMPUmhlaW5sYW5k
+LVBmYWx6MRAwDgYDVQQHEwdOZXV3aWVkMRQwEgYDVQQKEwtBTU9PTUEgR21iSDEX
+MBUGA1UEAxMOR2VtZWluc2NoYWZ0IDQxHTAbBgkqhkiG9w0BCQEWDmluZm9AYW1v
+b21hLmRlggkAobVdocVniAQwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOB
+gQCBIgWoP8YiP6tm8rhb81k6myP4ONO4vOaUz9bsadHDWNCTjiQxvo4uVYqTMLKa
+Bc7S0VpyvSg7/eGsSWxIUwdn6dUPdo51juGnnJ9dK9DuiNPHb0HP3UJo1gCWgs1v
+EnVGfKDdu9FfdNcQtIb28UfF8Pw8WA6mmhQOOh0M9d3ayQ==
+-----END CERTIFICATE-----
+
diff --git a/misc/etc/ssl/amooma/server_key.pem b/misc/etc/ssl/amooma/server_key.pem
new file mode 100644
index 0000000..0c8e74e
--- /dev/null
+++ b/misc/etc/ssl/amooma/server_key.pem
@@ -0,0 +1,16 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXQIBAAKBgQDC9G3px4ew18PHIm8HJ3yXc9rxqM5uSn1qhjdoWM0zC0Qcue9k
+V+5ZUq356yjBbs5wXi3SEWfucXxhnwnzeIqvMeO6y0BiUVsClbqziRCho/hgHPTJ
+tZzjt6Mpl3D/9yeFKbah5lJ5qNm0T00ybpcSXC7w2Xv9ckji1DDtGo62fQIDAQAB
+AoGAHM0jl9AEednGcJrjsDjjLTTOebkolh6nHJ+re9zyo8HcVCob9cUPz15pmWxm
+Xv1RvkQLnOc5ZX6ak4l9XNzIEAvQXNRFXwCOyfpffx/8QhfrG0v2G+K2QG52VxQj
+tqnRdLf8HEhCmrJCMvMEAuQkAiirIMTFcaaP1CBbCilr8nkCQQDnSYxEfXMYi4iq
+9Xjwn8Ayh1koXFUY+5/0u9SGqzTeTxW1QN2hGhehsd0vlv4cJppcuL4Z+2VYqLQc
+zXDZo/MLAkEA18kSHLp+HCd1BW/JEoIQqWlTw6SRx+IsUN7UmnSZS4C+UPRbtq5I
+nzgzonZufOEmzoMdwbe9EHAl087f0UfxlwJBAKNIBhGYKvgqEdr3n2Dotuw1J1la
+De2sPpmtPPWxyoojdOTYHV8Np59MjSV6yHyhOBq7heGb3EmCGF25H7FWkE8CQGUN
+aakAgPxoUfn4zp4XQPxFMhAF6qtDtOMuZzvp7LwaD4ZT2PtlBOdjZ3LmqXlb61N8
+vZuxkx22l1BoqhIU8gMCQQDJsQr5y8UWamYZrNv5YRNnm8aGgJ83Gx3n5b8bKqjh
+TM8hqJfFTIfHr90GhHyok1aVkjF+sUtydX1R85IHTDz5
+-----END RSA PRIVATE KEY-----
+
diff --git a/misc/example/apache-gs5.conf b/misc/example/apache-gs5.conf
new file mode 100644
index 0000000..ef81952
--- /dev/null
+++ b/misc/example/apache-gs5.conf
@@ -0,0 +1,24 @@
+
+ LoadModule passenger_module /usr/local/rvm/gems/ruby-1.9.2-p290/gems/passenger-3.0.11/ext/apache2/mod_passenger.so
+ PassengerRoot /usr/local/rvm/gems/ruby-1.9.2-p290/gems/passenger-3.0.11
+ PassengerRuby /usr/local/rvm/wrappers/ruby-1.9.2-p290/ruby
+
+
+
+ <VirtualHost *:443>
+ ErrorLog "|/usr/bin/logger -t apache -i -p local6.notice"
+ CustomLog "|/usr/bin/logger -t apache -i -p local6.notice" combined
+
+
+ DocumentRoot /opt/GS5/public
+ PassengerAppRoot /opt/GS5
+ RailsEnv development
+ <Directory /opt/GS5/public>
+ AllowOverride all
+ Options -MultiViews
+ Options FollowSymLinks
+ </Directory>
+ SSLEngine on
+ SSLCertificateFile /etc/ssl/amooma/server_cert.pem
+ SSLCertificateKeyFile /etc/ssl/amooma/server_key.pem
+ </VirtualHost>
diff --git a/misc/example/nginx b/misc/example/nginx
new file mode 100644
index 0000000..ccb2bbb
--- /dev/null
+++ b/misc/example/nginx
@@ -0,0 +1,73 @@
+#!/bin/sh
+
+#####################################################################
+# nginx
+# Start Script
+# (c) AMOOMA GmbH 2012
+#####################################################################
+
+### BEGIN INIT INFO
+# Provides: nginx
+# Required-Start: freeswitch
+# Required-Stop: freeswitch
+# Default-Start: 2 3 4 5
+# Default-Stop: 0 1 6
+# Short-Description: starts nginx
+# Description: starts nginx
+#
+### END INIT INFO
+
+DAEMON=/opt/nginx/sbin/nginx
+EXECUTABLE=`basename 'nginx'`
+NAME=nginx
+DESC=nginx
+ARGS=""
+
+if ! [ -x $DAEMON ] ; then
+ echo "ERROR: $DAEMON not found"
+ exit 1
+fi
+
+set -e
+
+. /lib/lsb/init-functions
+
+case "$1" in
+ start)
+ echo -n "Starting $DESC: "
+ start-stop-daemon --start --quiet --pidfile /opt/nginx/logs/$NAME.pid \
+ --exec $DAEMON -- $DAEMON_OPTS
+ echo "$NAME."
+ ;;
+ stop)
+ echo -n "Stopping $DESC: "
+ start-stop-daemon --stop --quiet --pidfile /opt/nginx/logs/$NAME.pid \
+ --exec $DAEMON
+ echo "$NAME."
+ ;;
+ restart|force-reload)
+ echo -n "Restarting $DESC: "
+ start-stop-daemon --stop --quiet --pidfile \
+ /opt/nginx/logs/$NAME.pid --exec $DAEMON
+ sleep 1
+ start-stop-daemon --start --quiet --pidfile \
+ /opt/nginx/logs/$NAME.pid --exec $DAEMON -- $DAEMON_OPTS
+ echo "$NAME."
+ ;;
+ reload)
+ echo -n "Reloading $DESC configuration: "
+ start-stop-daemon --stop --signal HUP --quiet --pidfile /opt/nginx/logs/$NAME.pid \
+ --exec $DAEMON
+ echo "$NAME."
+ ;;
+ status)
+ status_of_proc -p /opt/nginx/logs/$NAME.pid "$DAEMON" nginx
+ ;;
+ *)
+ N=/etc/init.d/$NAME
+ echo "Usage: $N {start|stop|restart|reload|force-reload|status}" >&2
+ exit 1
+ ;;
+esac
+
+exit 0
diff --git a/misc/example/xml-interface.txt b/misc/example/xml-interface.txt
new file mode 100644
index 0000000..9f05540
--- /dev/null
+++ b/misc/example/xml-interface.txt
@@ -0,0 +1,25 @@
+User anlegen
+============
+
+echo '<row><UserName>2222</UserName><LastName>Meyer</LastName><FirstName>Fritz</FirstName><PhoneOffice>+49 228 1234567</PhoneOffice><VoipNr>665544</VoipNr><CellPhone>+49 171 123456</CellPhone><Fax>+49 228 1234444</Fax><Email>fritz.meier@example.com</Email><PIN>123456</PIN><PIN_LastUpdate>2010-11-22T07:09:07.5256939</PIN_LastUpdate><Photo>example.jpg</Photo></row>' | curl -X POST -H 'Content-type: text/xml' -d @- http://0.0.0.0:3000/api/rows
+
+Userdaten abrufen
+=================
+curl -H 'Content-type: text/xml' http://0.0.0.0:3000/api/rows/1.xml
+
+User löschen
+============
+
+curl -i -H "Accept: application/xml" -X DELETE http://0.0.0.0:3000/api/rows/1
+
+If only the user name is known:
+curl -i -H "Accept: application/xml" -X DELETE http://0.0.0.0:3000/api/rows/999999?user_name=12345
+
+
+User updaten
+============
+
+curl -i -X PUT -H 'Content-Type: application/xml' -d '<row><PhoneOffice>+49 228 5555555</PhoneOffice><VoipNr>665544</VoipNr></row>' http://localhost:3000/api/rows/1
+
+If only the user name is known:
+curl -i -X PUT -H 'Content-Type: application/xml' -d '<row><PhoneOffice>+49 228 5555555</PhoneOffice><VoipNr>665544</VoipNr></row>' http://localhost:3000/api/rows/999999?user_name=12345 \ No newline at end of file
diff --git a/misc/freeswitch/conf/freeswitch.xml b/misc/freeswitch/conf/freeswitch.xml
new file mode 100644
index 0000000..04369a7
--- /dev/null
+++ b/misc/freeswitch/conf/freeswitch.xml
@@ -0,0 +1,759 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="freeswitch/xml">
+ <X-PRE-PROCESS cmd="set" data="sound_prefix=/opt/freeswitch/sounds/en/us/callie"/>
+ <X-PRE-PROCESS cmd="set" data="hold_music=local_stream://moh"/>
+ <X-PRE-PROCESS cmd="set" data="use_profile=internal"/>
+ <X-PRE-PROCESS cmd="set" data="send_silence_when_idle=400"/>
+ <X-PRE-PROCESS cmd="set" data="de-ring=%(1000,4000,425.0)"/>
+ <section name="languages" description="Language Management">
+ <language name="en" say-module="en" sound-prefix="/opt/freeswitch/sounds/en/us/callie">
+ <phrases>
+ <macros>
+ <macro name="voicemail_hello">
+ <input pattern="(.*)">
+ <match>
+ <action function="play-file" data="voicemail/vm-hello.wav"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_enter_id">
+ <input pattern="(.*)">
+ <match>
+ <action function="play-file" data="voicemail/vm-enter_id.wav"/>
+ <action function="say" data="$1" method="pronounced" type="name_spelled"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_enter_pass">
+ <input pattern="(.*)">
+ <match>
+ <action function="play-file" data="voicemail/vm-enter_pass.wav"/>
+ <action function="say" data="$1" method="pronounced" type="name_spelled"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_fail_auth">
+ <input pattern="(.*)">
+ <match>
+ <action function="play-file" data="voicemail/vm-fail_auth.wav"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_goodbye">
+ <input pattern="(.*)">
+ <match>
+ <action function="play-file" data="voicemail/vm-goodbye.wav"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_abort">
+ <input pattern="(.*)">
+ <match>
+ <action function="play-file" data="voicemail/vm-abort.wav"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_message_count">
+ <input pattern="^(1):(.*)$" break_on_match="true">
+ <match>
+ <action function="play-file" data="voicemail/vm-you_have.wav"/>
+ <action function="say" data="$1" method="pronounced" type="items"/>
+ <action function="play-file" data="voicemail/vm-$2.wav"/>
+ <action function="play-file" data="voicemail/vm-message.wav"/>
+ </match>
+ </input>
+ <input pattern="^(\d+):(.*)$">
+ <match>
+ <action function="play-file" data="voicemail/vm-you_have.wav"/>
+ <action function="say" data="$1" method="pronounced" type="items"/>
+ <action function="play-file" data="voicemail/vm-$2.wav"/>
+ <action function="play-file" data="voicemail/vm-messages.wav"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_menu">
+ <input pattern="^([0-9#*]):([0-9#*]):([0-9#*]):([0-9#*])$">
+ <match>
+ <action function="play-file" data="voicemail/vm-listen_new.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$1" method="pronounced" type="name_spelled"/>
+ <action function="execute" data="sleep(100)"/>
+ <action function="play-file" data="voicemail/vm-listen_saved.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$2" method="pronounced" type="name_spelled"/>
+ <action function="execute" data="sleep(100)"/>
+ <action function="play-file" data="voicemail/vm-advanced.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$3" method="pronounced" type="name_spelled"/>
+ <action function="execute" data="sleep(100)"/>
+ <action function="play-file" data="voicemail/vm-to_exit.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$4" method="pronounced" type="name_phonetic"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_config_menu">
+ <input pattern="^([0-9#*]):([0-9#*]):([0-9#*]):([0-9#*]):([0-9#*])$">
+ <match>
+ <action function="play-file" data="voicemail/vm-to_record_greeting.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$1" method="pronounced" type="name_spelled"/>
+ <action function="execute" data="sleep(100)"/>
+ <action function="play-file" data="voicemail/vm-choose_greeting.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$2" method="pronounced" type="name_spelled"/>
+ <action function="execute" data="sleep(100)"/>
+ <action function="play-file" data="voicemail/vm-record_name2.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$3" method="pronounced" type="name_spelled"/>
+ <action function="execute" data="sleep(100)"/>
+ <action function="play-file" data="voicemail/vm-change_password.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$4" method="pronounced" type="name_spelled"/>
+ <action function="execute" data="sleep(100)"/>
+ <action function="play-file" data="voicemail/vm-main_menu.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$5" method="pronounced" type="name_spelled"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_record_name">
+ <input pattern="^(.*)$">
+ <match>
+ <action function="play-file" data="voicemail/vm-record_name1.wav"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_record_file_check">
+ <input pattern="^([0-9#*]):([0-9#*]):([0-9#*])$">
+ <match>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$1" method="pronounced" type="name_spelled"/>
+ <action function="play-file" data="voicemail/vm-listen_to_recording.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$2" method="pronounced" type="name_spelled"/>
+ <action function="play-file" data="voicemail/vm-save_recording.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$3" method="pronounced" type="name_spelled"/>
+ <action function="play-file" data="voicemail/vm-rerecord.wav"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_record_urgent_check">
+ <input pattern="^([0-9#*]):([0-9#*])$">
+ <match>
+ <action function="play-file" data="voicemail/vm-mark-urgent.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$1" method="pronounced" type="name_spelled"/>
+ <action function="play-file" data="voicemail/vm-continue.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$2" method="pronounced" type="name_spelled"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_forward_prepend">
+ <input pattern="^([0-9#*]):([0-9#*])$">
+ <match>
+ <action function="play-file" data="voicemail/vm-forward_add_intro.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$1" method="pronounced" type="name_spelled"/>
+ <action function="play-file" data="voicemail/vm-send_message_now.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$2" method="pronounced" type="name_spelled"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_forward_message_enter_extension">
+ <input pattern="^([0-9#*])$">
+ <match>
+ <action function="play-file" data="voicemail/vm-forward_enter_ext.wav"/>
+ <action function="play-file" data="voicemail/vm-followed_by.wav"/>
+ <action function="say" data="$1" method="pronounced" type="name_spelled"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_invalid_extension">
+ <input pattern="^(.*)$">
+ <match>
+ <action function="play-file" data="voicemail/vm-that_was_an_invalid_ext.wav"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_listen_file_check">
+ <input pattern="^([0-9#*]):([0-9#*]):([0-9#*]):([0-9#*]):([0-9#*]):([0-9#*]):(.*)$">
+ <match>
+ <action function="play-file" data="voicemail/vm-listen_to_recording.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$1" method="pronounced" type="name_spelled"/>
+ <action function="play-file" data="voicemail/vm-save_recording.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$2" method="pronounced" type="name_spelled"/>
+ <action function="play-file" data="voicemail/vm-delete_recording.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$3" method="pronounced" type="name_spelled"/>
+ <action function="play-file" data="voicemail/vm-forward_to_email.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$4" method="pronounced" type="name_spelled"/>
+ <action function="play-file" data="voicemail/vm-return_call.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$5" method="pronounced" type="name_spelled"/>
+ <action function="play-file" data="voicemail/vm-to_forward.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$6" method="pronounced" type="name_spelled"/>
+ </match>
+ </input>
+ <input pattern="^([0-9#*]):([0-9#*]):([0-9#*]):([0-9#*]):([0-9#*]):([0-9#*])$">
+ <match>
+ <action function="play-file" data="voicemail/vm-listen_to_recording.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$1" method="pronounced" type="name_spelled"/>
+ <action function="play-file" data="voicemail/vm-save_recording.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$2" method="pronounced" type="name_spelled"/>
+ <action function="play-file" data="voicemail/vm-delete_recording.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$3" method="pronounced" type="name_spelled"/>
+ <action function="play-file" data="voicemail/vm-return_call.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$5" method="pronounced" type="name_spelled"/>
+ <action function="play-file" data="voicemail/vm-to_forward.wav"/>
+ <action function="play-file" data="voicemail/vm-press.wav"/>
+ <action function="say" data="$6" method="pronounced" type="name_spelled"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_choose_greeting">
+ <input pattern="^(.*)$">
+ <match>
+ <action function="play-file" data="voicemail/vm-choose_greeting_choose.wav"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_choose_greeting_fail">
+ <input pattern="^(.*)$">
+ <match>
+ <action function="play-file" data="voicemail/vm-choose_greeting_fail.wav"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_record_greeting">
+ <input pattern="^(.*)$">
+ <match>
+ <action function="play-file" data="voicemail/vm-record_greeting.wav"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_record_message">
+ <input pattern="^(.*)$">
+ <match>
+ <action function="play-file" data="voicemail/vm-record_message.wav"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_greeting_selected">
+ <input pattern="^(\d+)$">
+ <match>
+ <action function="play-file" data="voicemail/vm-greeting.wav"/>
+ <action function="say" data="$1" method="pronounced" type="items"/>
+ <action function="play-file" data="voicemail/vm-selected.wav"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_play_greeting">
+ <input pattern="^(\d+)$">
+ <match>
+ <action function="play-file" data="voicemail/vm-person.wav"/>
+ <action function="say" data="$1" method="pronounced" type="name_spelled"/>
+ <action function="play-file" data="voicemail/vm-not_available.wav"/>
+ </match>
+ </input>
+ <input pattern="^name:(.+)$">
+ <match>
+ <action function="play-file" data="$1"/>
+ <action function="play-file" data="voicemail/vm-not_available.wav"/>
+ </match>
+ </input>
+ <input pattern="^greeting:(.+)$">
+ <match>
+ <action function="play-file" data="$1"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_say_number">
+ <input pattern="^(\d+)$">
+ <match>
+ <action function="say" data="$1" method="pronounced" type="items"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_say_message_number">
+ <input pattern="^([a-z]+):(\d+)$">
+ <match>
+ <action function="play-file" data="voicemail/vm-$1.wav"/>
+ <action function="play-file" data="voicemail/vm-message_number.wav"/>
+ <action function="say" data="$2" method="pronounced" type="items"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_say_phone_number">
+ <input pattern="^(.*)$">
+ <match>
+ <action function="say" data="$1" method="pronounced" type="name_spelled"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_say_name">
+ <input pattern="^(.*)$">
+ <match>
+ <action function="say" data="$1" method="pronounced" type="name_spelled"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_ack">
+ <input pattern="^(too-small)$">
+ <match>
+ <action function="play-file" data="voicemail/vm-too-small.wav"/>
+ </match>
+ </input>
+ <input pattern="^(deleted)$">
+ <match>
+ <action function="play-file" data="voicemail/vm-message.wav"/>
+ <action function="play-file" data="voicemail/vm-$1.wav"/>
+ </match>
+ </input>
+ <input pattern="^(saved)$">
+ <match>
+ <action function="play-file" data="voicemail/vm-message.wav"/>
+ <action function="play-file" data="voicemail/vm-$1.wav"/>
+ </match>
+ </input>
+ <input pattern="^(emailed)$">
+ <match>
+ <action function="play-file" data="voicemail/vm-message.wav"/>
+ <action function="play-file" data="voicemail/vm-$1.wav"/>
+ </match>
+ </input>
+ <input pattern="^(marked-urgent)$">
+ <match>
+ <action function="play-file" data="voicemail/vm-message.wav"/>
+ <action function="play-file" data="voicemail/vm-$1.wav"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_say_date">
+ <input pattern="^(.*)$">
+ <match>
+ <action function="say" data="$1" method="pronounced" type="current_date_time"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="voicemail_disk_quota_exceeded">
+ <input pattern="^(.*)$">
+ <match>
+ <action function="play-file" data="voicemail/vm-mailbox_full.wav"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="valet_announce_ext">
+ <input pattern="^([^\:]+):(.*)$">
+ <match>
+ <action function="say" data="$2" method="pronounced" type="name_spelled"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="valet_lot_full">
+ <input pattern="^(.*)$">
+ <match>
+ <action function="play-file" data="tone_stream://%(275,10,600);%(275,100,300)"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="valet_lot_empty">
+ <input pattern="^(.*)$">
+ <match>
+ <action function="play-file" data="tone_stream://%(275,10,600);%(275,100,300)"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="logged_in">
+ <input pattern="^(.*)$">
+ <match>
+ <action function="play-file" data="ivr/ivr-you_are_now_logged_in.wav"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="logged_out">
+ <input pattern="^(.*)$">
+ <match>
+ <action function="play-file" data="ivr/ivr-you_are_now_logged_out.wav"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="acd_announce_position_enter">
+ <input pattern="^([0-9]+)$">
+ <match>
+ <action function="play-file" data="ivr/ivr-you_are_number.wav"/>
+ <action function="say" data="$1" method="pronounced" type="number"/>
+ <action function="play-file" data="ivr/ivr-in_line.wav"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="acd_announce_position_change">
+ <input pattern="^1$">
+ <match>
+ <action function="play-file" data="ivr/ivr-you_are_number.wav"/>
+ <action function="say" data="1" method="pronounced" type="number"/>
+ <action function="play-file" data="ivr/ivr-in_line.wav"/>
+ <action function="break"/>
+ </match>
+ </input>
+ <input pattern="^([0-9]+)$">
+ <match>
+ <action function="play-file" data="ivr/ivr-you_are_number.wav"/>
+ <action function="say" data="$1" method="pronounced" type="number"/>
+ <action function="play-file" data="ivr/ivr-in_line.wav"/>
+ <action function="play-file" data="ivr/ivr-thank_you_for_holding.wav"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="acd_announce_position_periodic">
+ <input pattern="^1$">
+ <match>
+ <action function="play-file" data="ivr/ivr-you_are_number.wav"/>
+ <action function="say" data="1" method="pronounced" type="number"/>
+ <action function="play-file" data="ivr/ivr-in_line.wav"/>
+ <action function="break"/>
+ </match>
+ </input>
+ <input pattern="^([0-9]+)$">
+ <match>
+ <action function="play-file" data="ivr/ivr-you_are_number.wav"/>
+ <action function="say" data="$1" method="pronounced" type="number"/>
+ <action function="play-file" data="ivr/ivr-in_line.wav"/>
+ <action function="play-file" data="ivr/ivr-thank_you_for_holding.wav"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="acd_announce_call_agents">
+ <input pattern="^(.*)$">
+ <match>
+ <action function="play-file" data="$1"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="acd_greeting">
+ <input pattern="^(.*)$">
+ <match>
+ <action function="play-file" data="$1"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="acd_goodbye">
+ <input pattern="^(.*)$">
+ <match>
+ <action function="play-file" data="$1"/>
+ </match>
+ </input>
+ </macro>
+ <macro name="acd_agent_status">
+ <input pattern="^active$">
+ <match>
+ <action function="play-file" data="ivr/ivr-you_are_now_logged_in.wav"/>
+ </match>
+ </input>
+ <input pattern="^inactive$">
+ <match>
+ <action function="play-file" data="ivr/ivr-you_are_now_logged_out.wav"/>
+ </match>
+ </input>
+ </macro>
+ </macros>
+ </phrases>
+ </language>
+ </section>
+ <section name="configuration" description="Gemeinschaft5 FreeSwitch configuration">
+ <configuration name="acl.conf" description="Network Lists">
+ <network-lists>
+ <list name="domains" default="deny">
+ <node type="allow" cidr="127.0.0.1/32"/>
+ </list>
+ </network-lists>
+ </configuration>
+ <configuration name="console.conf" description="Console Logger">
+ <mappings>
+ <map name="all" value="console,debug,info,notice,warning,err,crit,alert"/>
+ </mappings>
+ <settings>
+ <param name="colorize" value="true"/>
+ <param name="loglevel" value="info"/>
+ </settings>
+ </configuration>
+ <configuration name="event_socket.conf" description="Socket Client">
+ <settings>
+ <param name="nat-map" value="false"/>
+ <param name="listen-ip" value="127.0.0.1"/>
+ <param name="listen-port" value="8021"/>
+ </settings>
+ </configuration>
+ <configuration name="fifo.conf" description="FIFO Configuration">
+ <settings>
+ <param name="delete-all-outbound-member-on-startup" value="false"/>
+ </settings>
+ <fifos>
+ </fifos>
+ </configuration>
+ <configuration name="local_stream.conf" description="stream files from local dir">
+ <directory name="default" path="/opt/freeswitch/sounds/music/16000">
+ <param name="rate" value="16000"/>
+ <param name="shuffle" value="true"/>
+ <param name="channels" value="1"/>
+ <param name="interval" value="20"/>
+ <param name="timer-name" value="soft"/>
+ </directory>
+ <directory name="moh" path="/opt/freeswitch/sounds/music/16000">
+ <param name="rate" value="16000"/>
+ <param name="shuffle" value="true"/>
+ <param name="channels" value="1"/>
+ <param name="interval" value="20"/>
+ <param name="timer-name" value="soft"/>
+ </directory>
+ <directory name="mohl" path="/opt/freeswitch/sounds/music/8000">
+ <param name="rate" value="8000"/>
+ <param name="shuffle" value="true"/>
+ <param name="channels" value="1"/>
+ <param name="interval" value="20"/>
+ <param name="timer-name" value="soft"/>
+ </directory>
+ </configuration>
+ <configuration name="logfile.conf" description="File Logging">
+ <settings>
+ <param name="rotate-on-hup" value="true"/>
+ </settings>
+ <profiles>
+ <profile name="default">
+ <settings>
+ <param name="logfile" value="/var/log/freeswitch/freeswitch.log"/>
+ <param name="rollover" value="10485760"/>
+ </settings>
+ <mappings>
+ <map name="all" value="debug,info,notice,warning,err,crit,alert"/>
+ </mappings>
+ </profile>
+ </profiles>
+ </configuration>
+ <configuration name="xml_rpc.conf" description="XML RPC">
+ <settings>
+ <param name="http-port" value="228080"/>
+ <param name="auth-realm" value="gemeinschaft"/>
+ <param name="auth-user" value="7ff020f74d99a1b88bd2"/>
+ <param name="auth-pass" value="85d13b5a56c55f7261cc"/>
+ </settings>
+ </configuration>
+ <configuration name="switch.conf" description="Core Configuration">
+ <cli-keybindings>
+ </cli-keybindings>
+ <settings>
+ <param name="colorize-console" value="true"/>
+ <param name="max-sessions" value="1000"/>
+ <param name="sessions-per-second" value="30"/>
+ <param name="loglevel" value="debug"/>
+ <param name="mailer-app" value="sendmail"/>
+ <param name="mailer-app-args" value="-t"/>
+ <param name="dump-cores" value="yes"/>
+ <param name="auto-create-schemas" value="true"/>
+ <param name="rtp-enable-zrtp" value="false"/>
+ <param name="rtp-start-port" value="16384" />
+ <param name="rtp-end-port" value="32768" />
+ <param name="core-db-dsn" value="gemeinschaft:gemeinschaft:gemeinschaft"/>
+ </settings>
+ </configuration>
+ <configuration name="spandsp.conf" description="Tone detector descriptors">
+ <descriptors>
+ <descriptor name="1">
+ <tone name="CED_TONE">
+ <element freq1="2100" freq2="0" min="500" max="0"/>
+ </tone>
+ <tone name="SIT">
+ <element freq1="950" freq2="0" min="256" max="400"/>
+ <element freq1="1400" freq2="0" min="256" max="400"/>
+ <element freq1="1800" freq2="0" min="256" max="400"/>
+ </tone>
+ <tone name="REORDER_TONE">
+ <element freq1="480" freq2="620" min="224" max="272"/>
+ <element freq1="0" freq2="0" min="224" max="272"/>
+ </tone>
+ <tone name="BUSY_TONE">
+ <element freq1="480" freq2="620" min="464" max="516"/>
+ <element freq1="0" freq2="0" min="464" max="516"/>
+ </tone>
+ </descriptor>
+ <descriptor name="44">
+ <tone name="CED_TONE">
+ <element freq1="2100" freq2="0" min="500" max="0"/>
+ </tone>
+ <tone name="SIT">
+ <element freq1="950" freq2="0" min="256" max="400"/>
+ <element freq1="1400" freq2="0" min="256" max="400"/>
+ <element freq1="1800" freq2="0" min="256" max="400"/>
+ </tone>
+ <tone name="REORDER_TONE">
+ <element freq1="400" freq2="0" min="368" max="416"/>
+ <element freq1="0" freq2="0" min="336" max="368"/>
+ <element freq1="400" freq2="0" min="256" max="288"/>
+ <element freq1="0" freq2="0" min="512" max="544"/>
+ </tone>
+ <tone name="BUSY_TONE">
+ <element freq1="400" freq2="0" min="352" max="384"/>
+ <element freq1="0" freq2="0" min="352" max="384"/>
+ <element freq1="400" freq2="0" min="352" max="384"/>
+ <element freq1="0" freq2="0" min="352" max="384"/>
+ </tone>
+ </descriptor>
+ <descriptor name="49">
+ <tone name="CED_TONE">
+ <element freq1="2100" freq2="0" min="500" max="0"/>
+ </tone>
+ <tone name="SIT">
+ <element freq1="900" freq2="0" min="256" max="400"/>
+ <element freq1="1400" freq2="0" min="256" max="400"/>
+ <element freq1="1800" freq2="0" min="256" max="400"/>
+ </tone>
+ <tone name="REORDER_TONE">
+ <element freq1="425" freq2="0" min="224" max="272"/>
+ <element freq1="0" freq2="0" min="224" max="272"/>
+ </tone>
+ <tone name="BUSY_TONE">
+ <element freq1="425" freq2="0" min="464" max="516"/>
+ <element freq1="0" freq2="0" min="464" max="516"/>
+ </tone>
+ </descriptor>
+ </descriptors>
+ </configuration>
+ <configuration name="fax.conf" description="FAX application configuration">
+ <settings>
+ <param name="use-ecm" value="true"/>
+ <param name="verbose" value="true"/>
+ <param name="disable-v17" value="false"/>
+ <param name="ident" value=""/>
+ <param name="header" value="GS4"/>
+ <param name="spool-dir" value="/opt/GS5/misc/fax"/>
+ <param name="file-prefix" value="fax_in_"/>
+ </settings>
+ </configuration>
+ <configuration name="modules.conf" description="Modules">
+ <modules>
+ <load module="mod_console"/>
+ <load module="mod_logfile"/>
+ <load module="mod_lua"/>
+ <!-- <load module="mod_xml_rpc"/> -->
+ <!-- <load module="mod_cdr_csv"/> -->
+ <load module="mod_event_socket"/>
+ <load module="mod_sofia"/>
+ <load module="mod_loopback"/>
+ <load module="mod_commands"/>
+ <load module="mod_conference"/>
+ <load module="mod_dptools"/>
+ <load module="mod_expr"/>
+ <!-- <load module="mod_fifo"/> -->
+ <load module="mod_voicemail"/>
+ <!-- <load module="mod_esf"/> -->
+ <!-- <load module="mod_fsv"/> -->
+ <load module="mod_valet_parking"/>
+ <!-- <load module="mod_curl"/> -->
+ <load module="mod_dialplan_xml"/>
+ <load module="mod_sndfile"/>
+ <load module="mod_native_file"/>
+ <load module="mod_local_stream"/>
+ <load module="mod_tone_stream"/>
+ <load module="mod_say_en"/>
+ <load module="mod_spandsp"/>
+ </modules>
+ </configuration>
+ <configuration name="post_load_modules.conf" description="Modules">
+ <modules>
+ </modules>
+ </configuration>
+ <configuration name="voicemail.conf" description="Voicemail">
+ <settings>
+ </settings>
+ <profiles>
+ <profile name="default">
+ <param name="file-extension" value="wav"/>
+ <param name="terminator-key" value="#"/>
+ <param name="max-login-attempts" value="3"/>
+ <param name="digit-timeout" value="10000"/>
+ <param name="min-record-len" value="3"/>
+ <param name="max-record-len" value="300"/>
+ <param name="max-retries" value="3"/>
+ <param name="tone-spec" value="%(1000, 0, 640)"/>
+ <param name="callback-dialplan" value="XML"/>
+ <param name="callback-context" value="default"/>
+ <param name="play-new-messages-key" value="1"/>
+ <param name="play-saved-messages-key" value="2"/>
+ <param name="login-keys" value="0"/>
+ <param name="main-menu-key" value="0"/>
+ <param name="config-menu-key" value="5"/>
+ <param name="record-greeting-key" value="1"/>
+ <param name="choose-greeting-key" value="2"/>
+ <param name="change-pass-key" value="6"/>
+ <param name="record-name-key" value="3"/>
+ <param name="record-file-key" value="3"/>
+ <param name="listen-file-key" value="1"/>
+ <param name="save-file-key" value="2"/>
+ <param name="delete-file-key" value="7"/>
+ <param name="undelete-file-key" value="8"/>
+ <param name="email-key" value="4"/>
+ <param name="pause-key" value="0"/>
+ <param name="restart-key" value="1"/>
+ <param name="ff-key" value="6"/>
+ <param name="rew-key" value="4"/>
+ <param name="skip-greet-key" value="#"/>
+ <param name="record-silence-threshold" value="200"/>
+ <param name="record-silence-hits" value="2"/>
+ <param name="web-template-file" value="web-vm.tpl"/>
+ <param name="operator-extension" value="operator XML default"/>
+ <param name="operator-key" value="9"/>
+ <param name="vmain-extension" value="vmain XML default"/>
+ <param name="vmain-key" value="*"/>
+ <param name="odbc-dsn" value="gemeinschaft:gemeinschaft:gemeinschaft"/>
+ <email>
+ <param name="notify-template-file" value="notify-voicemail.tpl"/>
+ <param name="template-file" value="voicemail.tpl"/>
+ <param name="date-fmt" value="%A, %B %d %Y, %I %M %p"/>
+ <param name="email-from" value="${voicemail_account}@${voicemail_domain}"/>
+ </email>
+ </profile>
+ </profiles>
+ </configuration>
+ <configuration name="lua.conf" description="LUA Configuration">
+ <settings>
+ <param name="script-directory" value="$${base_dir}/scripts/?.lua"/>
+ <param name="xml-handler-script" value="configuration.lua"/>
+ <param name="xml-handler-bindings" value="directory|configuration"/>
+ <param name="startup-script" value="fax_daemon.lua"/>
+ <param name="startup-script" value="event_manager.lua"/>
+ </settings>
+ </configuration>
+ </section>
+ <section name="dialplan" description="Regex/XML dialplan">
+ <context name="default">
+ <extension name="invoke_default_dialplan" continue="true">
+ <condition>
+ <action application="set" data="script=${lua(dialplan_default.lua)}"/>
+ </condition>
+ </extension>
+ <extension name="transfer_loop" continue="false">
+ <condition field="endpoint_disposition" expression="BLIND_TRANSFER">
+ <action application="transfer" data=" XML default"/>
+ </condition>
+ </extension>
+ </context>
+ <context name="redirected">
+ <extension name="redirected" continue="true">
+ <condition>
+ <action application="transfer" data="${sip_redirect_contact_user_0} XML default"/>
+ </condition>
+ </extension>
+ </context>
+ </section>
+</document>
diff --git a/misc/freeswitch/scripts/acd_wait.lua b/misc/freeswitch/scripts/acd_wait.lua
new file mode 100644
index 0000000..fd16bea
--- /dev/null
+++ b/misc/freeswitch/scripts/acd_wait.lua
@@ -0,0 +1,45 @@
+-- Gemeinschaft 5: acd call handler
+-- (c) AMOOMA GmbH 2012
+--
+
+local caller_uuid = argv[1];
+local acd_id = tonumber(argv[2]);
+local timeout = tonumber(argv[3]);
+local retry_timeout = tonumber(argv[4]);
+local acd_caller_id = tonumber(argv[5]);
+
+-- initialize logging
+require 'common.log'
+local log = common.log.Log:new{ prefix = '### [' .. caller_uuid .. '] ' };
+
+if not acd_id then
+ log:error('ACD_WAIT - automaticcalldistributor=', acd_id, ' not specified');
+ return;
+end
+
+-- connect to database
+require 'common.database'
+local database = common.database.Database:new{ log = log }:connect();
+if not database:connected() then
+ log:critical('ACD_WAIT - database connect failed');
+ database:release();
+ return;
+end
+
+require 'dialplan.acd'
+local acd = dialplan.acd.AutomaticCallDistributor:new{ log = log, database = database }:find_by_id(acd_id);
+
+if not acd then
+ log:error('ACD_WAIT - automaticcalldistributor=', acd_id, ' not found');
+ database:release();
+ return;
+end
+
+log:debug('ACD_WAIT ', acd_id, ' - start');
+acd:wait_turn(caller_uuid, acd_caller_id, timeout, retry_timeout);
+log:debug('ACD_WAIT ', acd_id, ' - end');
+
+-- release database
+if database then
+ database:release();
+end
diff --git a/misc/freeswitch/scripts/common/call_forwarding.lua b/misc/freeswitch/scripts/common/call_forwarding.lua
new file mode 100644
index 0000000..3942d05
--- /dev/null
+++ b/misc/freeswitch/scripts/common/call_forwarding.lua
@@ -0,0 +1,47 @@
+-- Gemeinschaft 5 module: call forwarding class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+CallForwarding = {}
+
+-- Create CallForwarding object
+function CallForwarding.new(self, arg, object)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self
+ self.log = arg.log;
+ self.database = arg.database;
+ self.record = arg.record;
+ self.domain = arg.domain;
+ return object;
+end
+
+-- Find call forwarding by id
+function CallForwarding.find_by_id(self, id)
+ local sql_query = 'SELECT * FROM `call_forwards` WHERE `id`= ' .. tonumber(id) .. ' LIMIT 1';
+ local record = nil
+
+ self.database:query(sql_query, function(entry)
+ record = entry;
+ end)
+
+ if record then
+ call_forwarding = CallForwarding:new(self)
+ call_forwarding.record = record
+ return call_forwarding
+ end
+
+ return nil
+end
+
+function CallForwarding.presence_set(self, presence_state)
+ require 'dialplan.presence'
+ local presence = dialplan.presence.Presence:new();
+
+ presence:init{log = self.log, accounts = { 'f-cftg-' .. tostring(self.record.id) }, domain = self.domain, uuid = 'call_forwarding_' .. tostring(self.record.id)};
+
+ return presence:set(presence_state);
+end
diff --git a/misc/freeswitch/scripts/common/call_history.lua b/misc/freeswitch/scripts/common/call_history.lua
new file mode 100644
index 0000000..c5bc0bf
--- /dev/null
+++ b/misc/freeswitch/scripts/common/call_history.lua
@@ -0,0 +1,140 @@
+-- Gemeinschaft 5 module: call_history class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+
+function camelize_type(account_type)
+ ACCOUNT_TYPES = {
+ sipaccount = 'SipAccount',
+ conference = 'Conference',
+ faxaccount = 'FaxAccount',
+ callthrough = 'Callthrough',
+ huntgroup = 'HuntGroup',
+ automaticcalldistributor = 'AutomaticCallDistributor',
+ }
+
+ return ACCOUNT_TYPES[account_type] or account_type;
+end
+
+
+CallHistory = {}
+
+-- Create CallHistory object
+function CallHistory.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.class = 'callhistory';
+ self.log = arg.log;
+ self.database = arg.database;
+ return object;
+end
+
+
+function CallHistory.insert_entry(self, call_history)
+ local keys = {}
+ local values = {}
+
+ call_history.created_at = 'NOW()';
+ call_history.updated_at = 'NOW()';
+
+ for key, value in pairs(call_history) do
+ table.insert(keys, key);
+ table.insert(values, value);
+ end
+
+ local sql_query = 'INSERT INTO `call_histories` (`' .. table.concat(keys, "`, `") .. '`) VALUES (' .. table.concat(values, ", ") .. ')';
+ local result = self.database:query(sql_query);
+ if not result then
+ self.log:error('[', call_history.caller_channel_uuid, '] CALL_HISTORY_SAVE - SQL: ', sql_query);
+ end
+ return result;
+end
+
+
+function CallHistory.insert_event(self, uuid, account_type, account_id, entry_type, event)
+ require 'common.str'
+ local call_history = {}
+
+ call_history.entry_type = common.str.to_sql(entry_type);
+ call_history.call_historyable_type = common.str.to_sql(camelize_type(account_type));
+ call_history.call_historyable_id = common.str.to_sql(account_id);
+ call_history.caller_channel_uuid = common.str.to_sql(uuid);
+ call_history.duration = common.str.to_sql(event:getHeader('variable_billsec'));
+ call_history.caller_id_number = common.str.to_sql(event:getHeader('variable_effective_caller_id_number'));
+ call_history.caller_id_name = common.str.to_sql(event:getHeader('variable_effective_caller_id_name'));
+ call_history.callee_id_number = common.str.to_sql(event:getHeader('variable_effective_callee_id_number'));
+ call_history.callee_id_name = common.str.to_sql(event:getHeader('variable_effective_callee_id_name'));
+ call_history.result = common.str.to_sql(event:getHeader('variable_hangup_cause'));
+ call_history.start_stamp = 'FROM_UNIXTIME(' .. math.floor(common.str.to_i(event:getHeader('Caller-Channel-Created-Time')) / 1000000) .. ')';
+ call_history.caller_account_type = common.str.to_sql(camelize_type(event:getHeader('variable_gs_caller_account_type') or event:getHeader('variable_gs_account_type')));
+ call_history.caller_account_id = common.str.to_sql(event:getHeader('variable_gs_caller_account_id') or event:getHeader('variable_gs_account_id'));
+ call_history.auth_account_type = common.str.to_sql(camelize_type(event:getHeader('variable_gs_auth_account_type')));
+ call_history.auth_account_id = common.str.to_sql(event:getHeader('variable_gs_auth_account_id'));
+ call_history.callee_account_type = common.str.to_sql(camelize_type(event:getHeader('variable_gs_destination_type')));
+ call_history.callee_account_id = common.str.to_sql(event:getHeader('variable_gs_destination_id'));
+ call_history.destination_number = common.str.to_sql(event:getHeader('variable_gs_destination_number'));
+ call_history.forwarding_service = common.str.to_sql(event:getHeader('variable_gs_forwarding_service'));
+
+ if common.str.to_s(event:getHeader('variable_gs_call_service')) == 'pickup' then
+ call_history.forwarding_service = common.str.to_sql('pickup');
+ end
+
+ self.log:info('[', uuid,'] CALL_HISTORY_SAVE ', entry_type,' - account: ', account_type, '=', account_id,
+ ', caller: ', call_history.caller_id_number, ' ', call_history.caller_id_name,
+ ', callee: ', call_history.callee_id_number, ' ', call_history.callee_id_name,
+ ', result: ', call_history.result
+ );
+
+ return self:insert_entry(call_history);
+end
+
+
+function CallHistory.insert_forwarded(self, uuid, account_type, account_id, caller, destination, result)
+ require 'common.str'
+
+ local call_history = {}
+
+ call_history.entry_type = common.str.to_sql('forwarded');
+ call_history.call_historyable_type = common.str.to_sql(camelize_type(account_type));
+ call_history.call_historyable_id = common.str.to_sql(account_id);
+ call_history.caller_channel_uuid = common.str.to_sql(uuid);
+
+ call_history.duration = common.str.to_sql(caller:to_i('billsec'));
+ call_history.caller_id_number = common.str.to_sql(caller.caller_id_number);
+ call_history.caller_id_name = common.str.to_sql(caller.caller_id_name);
+ call_history.callee_id_number = common.str.to_sql(caller.callee_id_number);
+ call_history.callee_id_name = common.str.to_sql(caller.callee_id_name);
+ call_history.result = common.str.to_sql(result.cause or 'UNSPECIFIED');
+ call_history.start_stamp = 'FROM_UNIXTIME(' .. math.floor(caller:to_i('created_time') / 1000000) .. ')';
+
+ if caller.account then
+ call_history.caller_account_type = common.str.to_sql(camelize_type(caller.account.class));
+ call_history.caller_account_id = common.str.to_sql(caller.account.id);
+ end
+
+ if caller.auth_account then
+ call_history.auth_account_type = common.str.to_sql(camelize_type(caller.auth_account.class));
+ call_history.auth_account_id = common.str.to_sql(caller.auth_account.id);
+ end
+
+ if destination then
+ call_history.callee_account_type = common.str.to_sql(camelize_type(destination.type));
+ call_history.callee_account_id = common.str.to_sql(destination.id);
+ call_history.destination_number = common.str.to_sql(destination.number);
+ end
+
+ call_history.forwarding_service = common.str.to_sql(caller.forwarding_service);
+
+ self.log:info('CALL_HISTORY_SAVE forwarded - account: ', account_type, '=', account_id,
+ ', service: ', call_history.forwarding_service,
+ ', caller: ', call_history.caller_id_number, ' ', call_history.caller_id_name,
+ ', callee: ', call_history.callee_id_number, ' ', call_history.callee_id_name,
+ ', result: ', call_history.result
+ );
+
+ return self:insert_entry(call_history);
+end
diff --git a/misc/freeswitch/scripts/common/conference.lua b/misc/freeswitch/scripts/common/conference.lua
new file mode 100644
index 0000000..d2bf829
--- /dev/null
+++ b/misc/freeswitch/scripts/common/conference.lua
@@ -0,0 +1,239 @@
+-- Gemeinschaft 5 module: conference class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+Conference = {}
+
+MEMBERS_MAX = 100;
+PIN_LENGTH_MAX = 10;
+PIN_LENGTH_MIN = 2;
+PIN_TIMEOUT = 4000;
+ANNOUNCEMENT_MAX_LEN = 10
+ANNOUNCEMENT_SILENCE_THRESHOLD = 500
+ANNOUNCEMENT_SILENCE_LEN = 3
+
+-- create conference object
+function Conference.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.class = 'conference';
+ self.log = arg.log;
+ self.database = arg.database;
+ self.record = arg.record;
+ self.max_members = 0;
+ return object;
+end
+
+-- find conference by id
+function Conference.find_by_id(self, id)
+ local sql_query = 'SELECT * FROM `conferences` WHERE `id`= ' .. tonumber(id) .. ' LIMIT 1';
+ local conference = nil;
+
+ self.database:query(sql_query, function(conference_entry)
+ conference = Conference:new(self);
+ conference.record = conference_entry;
+ conference.id = tonumber(conference_entry.id);
+ conference.uuid = conference_entry.uuid;
+ conference.max_members = tonumber(conference.record.max_members) or MEMBERS_MAX;
+ end)
+
+ return conference;
+end
+
+-- find invitee by phone numbers
+function Conference.find_invitee_by_numbers(self, phone_numbers)
+ if not self.record then
+ return false
+ end
+
+ local sql_query = string.format(
+ "SELECT `conference_invitees`.`pin` AS `pin`, `conference_invitees`.`speaker` AS `speaker`, `conference_invitees`.`moderator` AS `moderator` " ..
+ "FROM `conference_invitees` JOIN `phone_numbers` ON `phone_numbers`.`phone_numberable_id` = `conference_invitees`.`id` " ..
+ "WHERE `phone_numbers`.`phone_numberable_type` = 'ConferenceInvitee' AND `conference_invitees`.`conference_id` = %d " ..
+ "AND `phone_numbers`.`number` IN ('%s') LIMIT 1", self.record.id, table.concat(phone_numbers, "','"));
+
+ local invitee = nil;
+
+ self.database:query(sql_query, function(conference_entry)
+ invitee = conference_entry;
+ end)
+
+ return invitee;
+end
+
+function Conference.count(self)
+ return tonumber(self.caller:result('conference ' .. self.record.id .. ' list count')) or 0;
+end
+
+-- Try to enter a conference
+function Conference.enter(self, caller, domain)
+ local cause = "NORMAL_CLEARING";
+ local pin = nil;
+ local flags = {'waste'};
+
+ self.caller = caller;
+
+ require "common.phone_number"
+ local phone_number_class = common.phone_number.PhoneNumber:new{log = self.log, database = self.database}
+ local phone_numbers = phone_number_class:list_by_owner(self.record.id, "Conference");
+
+ -- Set conference presence
+ require "dialplan.presence"
+ local presence = dialplan.presence.Presence:new();
+ presence:init{ log = log, accounts = phone_numbers, domain = domain, uuid = "conference_" .. self.record.id };
+
+ local conference_count = self:count();
+
+ -- Check if conference is full
+ if conference_count >= self.max_members then
+ presence:early();
+ self.log:debug(string.format("full conference %s (\"%s\"), members: %d, members allowed: %d", self.record.id, self.record.name, conference_count, self.max_members));
+
+ if (tonumber(self.record.conferenceable_id) == caller.account_owner_id)
+ and (self.record.conferenceable_type == caller.account_owner_type) then
+ self.log:debug("Allow owner of this conterence to enter a full conference");
+ else
+ cause = "CALL_REJECTED";
+ caller:hangup(cause);
+ return cause;
+ end;
+ end
+
+ -- Check if conference is within time frame
+ if self.record.start and self.record['end'] then
+ local d = {}
+ _,_,d.year,d.month,d.day,d.hour,d.min,d.sec=string.find(self.record.start, "(%d+)-(%d+)-(%d+) (%d+):(%d+):(%d+)");
+
+ local conference_start = os.time(d);
+ _,_,d.year,d.month,d.day,d.hour,d.min,d.sec=string.find(self.record['end'], "(%d+)-(%d+)-(%d+) (%d+):(%d+):(%d+)");
+ local conference_end = os.time(d);
+ local now = os.time(os.date("!*t", os.time()));
+
+ log:debug("conference - open: " .. os.date("%c",conference_start) .. " by " .. os.date("%c",conference_end) .. ", now: " .. os.date("%c",now));
+
+ if now < conference_start or now > conference_end then
+ cause = "CALL_REJECTED";
+ caller:hangup(cause);
+ return cause;
+ end
+ end
+
+ require 'common.str'
+ -- Owner ist always moderator
+ if (tonumber(self.record.conferenceable_id) == caller.account_owner_id) and (self.record.conferenceable_type == caller.account_owner_type) then
+ table.insert(flags, 'moderator');
+ log:debug("is owner - conference: " .. self.record.id .. ", owner: " .. caller.account_owner_type .. ":" .. caller.account_owner_id);
+ else
+ local invitee = self:find_invitee_by_numbers(caller.caller_phone_numbers);
+
+ if not common.str.to_b(self.record.open_for_anybody) and not invitee then
+ log:debug(string.format("conference %s (\"%s\"), caller %s not allowed to enter this conference", self.record.id, self.record.name, caller.caller_phone_number));
+ cause = "CALL_REJECTED";
+ caller:hangup(cause);
+ return cause;
+ end
+
+ if invitee then
+ log:debug("conference " .. self.record.id .. " member invited - speaker: " .. invitee.speaker .. ", moderator: " .. invitee.moderator);
+ if common.str.to_b(invitee.moderator) then
+ table.insert(flags, 'moderator');
+ end
+ if not common.str.to_b(invitee.speaker) then
+ table.insert(flags, 'mute');
+ end
+ pin = invitee.pin;
+ else
+ log:debug("conference " .. self.record.id .. " caller not invited");
+ end
+ end
+
+ if not pin and self.record.pin then
+ pin = self.record.pin
+ end
+
+ caller:answer();
+ caller:sleep(1000);
+ caller.session:streamFile('conference/conf-welcome.wav');
+
+ if pin and pin ~= "" then
+ local digits = "";
+ for i = 1, 3, 1 do
+ if digits == pin then
+ break
+ elseif digits ~= "" then
+ caller.session:streamFile('conference/conf-bad-pin.wav');
+ end
+ digits = caller.session:read(PIN_LENGTH_MIN, PIN_LENGTH_MAX, 'conference/conf-enter_conf_pin.wav', PIN_TIMEOUT, '#');
+ end
+ if digits ~= pin then
+ caller.session:streamFile("conference/conf-goodbye.wav");
+ return "CALL_REJECTED";
+ end
+ end
+
+ self.log:debug(string.format("entering conference %s - name: \"%s\", flags: %s, members: %d, max. members: %d",
+ self.record.id, self.record.name, table.concat(flags, ','), conference_count, self.max_members));
+
+ -- Members count will be incremented in a few milliseconds, set presence
+ if (conference_count + 1) >= self.max_members then
+ presence:early();
+ else
+ presence:confirmed();
+ end
+
+ -- Enter the conference
+ local name_file = nil;
+
+ -- Record caller's name
+ if common.str.to_b(self.record.announce_new_member_by_name) or common.str.to_b(self.record.announce_left_member_by_name) then
+ local uid = session:get_uuid();
+ name_file = "/tmp/conference_caller_name_" .. uid .. ".wav";
+ caller.session:streamFile("voicemail/vm-record_name1.wav");
+ caller.session:execute("playback", "tone_stream://%(1000,0,500)");
+ session:recordFile(name_file, ANNOUNCEMENT_MAX_LEN, ANNOUNCEMENT_SILENCE_THRESHOLD, ANNOUNCEMENT_SILENCE_LEN);
+ caller.session:streamFile(name_file);
+ end
+
+ -- Play entering caller's name if recorded
+ if name_file and (self:count() > 0) and common.str.to_b(self.record.announce_new_member_by_name) then
+ caller.session:execute('set',"result=${conference(" .. self.record.id .. " play ".. name_file .. ")}");
+ caller.session:execute('set',"result=${conference(" .. self.record.id .. " play conference/conf-has_joined.wav)}");
+ else
+ -- Ensure a surplus "#" digit is not passed to the conference
+ caller.session:read(1, 1, '', 1000, "#");
+ end
+
+ local result = caller.session:execute('conference', self.record.id .. "@profile_" .. self.record.id .. "++flags{" .. table.concat(flags, '|') .. "}");
+ self.log:debug('exited conference - result: ' .. tostring(result));
+ caller.session:streamFile("conference/conf-goodbye.wav")
+
+ -- Play leaving caller's name if recorded
+ if name_file then
+ if (self:count() > 0) and common.str.to_b(self.record.announce_left_member_by_name) then
+ if (self:count() == 1) then
+ caller.session:sleep(3000);
+ end
+ caller.session:execute('set',"result=${conference(" .. self.record.id .. " play ".. name_file .. ")}");
+ caller.session:execute('set',"result=${conference(" .. self.record.id .. " play conference/conf-has_left.wav)}");
+ end
+ os.remove(name_file);
+ end
+
+ -- Set presence according to member count
+ conference_count = self:count();
+ if conference_count >= self.max_members then
+ presence:early();
+ elseif conference_count > 0 then
+ presence:confirmed();
+ else
+ presence:terminated();
+ end
+
+ cause = "NORMAL_CLEARING";
+ caller.session:hangup(cause);
+ return cause;
+end
diff --git a/misc/freeswitch/scripts/common/configuration_file.lua b/misc/freeswitch/scripts/common/configuration_file.lua
new file mode 100644
index 0000000..67e1f3b
--- /dev/null
+++ b/misc/freeswitch/scripts/common/configuration_file.lua
@@ -0,0 +1,70 @@
+-- Gemeinschaft 5 module: configuration file
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+function ignore_comments(line)
+ return line:gsub(';+([^;]*)', function(entry)
+ return '';
+ end);
+end
+
+-- parse configuration
+function parse(lines, filter_section_name)
+ require 'common.str'
+ local section = {}
+ local root = { [true] = section }
+
+ for line in lines do
+ if line then
+ local ignore_line = false;
+ line = ignore_comments(line);
+
+ line:gsub('^%s*%[(.-)%]%s*$', function(section_name)
+ if tostring(section_name):match('%=false$') then
+ section = {}
+ else
+ root[common.str.strip(section_name)] = {};
+ section = root[common.str.strip(section_name)];
+ end
+ ignore_line = true;
+ end);
+
+ if not ignore_line then
+ key, value = common.str.partition(line, '=');
+ if value and key and not common.str.strip(key):match('%s') then
+ section[common.str.strip(key)] = common.str.strip(value);
+ else
+ line = common.str.strip(line);
+ if not common.str.blank(line) then
+ if line:match(',') then
+ table.insert(section, common.str.strip_to_a(line, ','));
+ else
+ table.insert(section, line);
+ end
+ end
+ end
+ end
+ end
+ end
+
+ if filter_section_name == false then
+ root[true] = nil;
+ elseif filter_section_name then
+ return root[filter_section_name];
+ end
+
+ return root;
+end
+
+-- retrieve configuration from file
+function get(file_name, filter_section_name)
+ local file = io.open(file_name);
+
+ if file then
+ local result = parse(file:lines(), filter_section_name);
+ file:close();
+ return result;
+ end
+end
diff --git a/misc/freeswitch/scripts/common/database.lua b/misc/freeswitch/scripts/common/database.lua
new file mode 100644
index 0000000..3692f84
--- /dev/null
+++ b/misc/freeswitch/scripts/common/database.lua
@@ -0,0 +1,151 @@
+-- Gemeinschaft 5 module: database class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+Database = {}
+
+DATABASE_DRIVER = 'mysql'
+
+function Database.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.class = 'database';
+ self.log = arg.log;
+ self.conn = nil;
+ return object;
+end
+
+
+function Database.connect(self, database_name, user_name, password, host_name)
+ local database_driver = nil;
+ if not (database_name and user_name and password) then
+ require 'common.configuration_file'
+ local config = common.configuration_file.get('/opt/freeswitch/scripts/ini/database.ini');
+ if config then
+ database_driver = config[true].driver
+ database_name = config[database_driver].database
+ user_name = config[database_driver].user
+ password = config[database_driver].password
+ host_name = config[database_driver].host
+ end
+ end
+
+ host_name = host_name or 'localhost';
+ database_driver = database_driver or DATABASE_DRIVER;
+
+ if database_driver == 'mysql' then
+ require "luasql.mysql"
+ self.env = luasql.mysql();
+ elseif database_driver == 'odbc' then
+ require "luasql.odbc"
+ self.env = luasql.odbc();
+ end
+
+ self.conn = self.env:connect(database_name, user_name, password, host_name);
+ self.conn_id = tostring(self.conn);
+ self.database_name = database_name;
+ self.user_name = user_name;
+ self.password = password;
+ self.host_name = host_name;
+
+ -- self.log:debug('DATABASE_CONNECT - connection: ', self.conn_id, ', environment: ', self.env);
+
+ return self;
+end
+
+
+function Database.reconnect(self)
+ self.conn = self.env:connect(self.database_name, self.user_name, self.password, self.host_name);
+ self.conn_id = tostring(self.conn);
+
+ if self.log then
+ self.log:info('DATABASE_RECONNECT - connection: ', self.conn_id, ', environment: ', self.env);
+ end
+
+ return self;
+end
+
+
+function Database.connected(self)
+ return self.conn;
+end
+
+
+function Database.query(self, sql_query, call_function)
+ local cursor = self.conn:execute(sql_query);
+
+ if cursor == nil and not self.conn:execute('SELECT @@VERSION') then
+ if self.log then
+ self.log:error('DATABASE_QUERY - lost connection: ', self.conn_id, ', environment: ', self.env, ', query: ', sql_query);
+ end
+ self:reconnect();
+
+ if call_function then
+ cursor = self.conn:execute(sql_query);
+ self.log:notice('DATABASE_QUERY - retry: ', sql_query);
+ end
+ end
+
+ if cursor and call_function then
+ repeat
+ row = cursor:fetch({}, 'a');
+ if row then
+ call_function(row);
+ end
+ until not row;
+ end
+
+ if type(cursor) == 'userdata' then
+ cursor:close();
+ end
+
+ return cursor;
+end
+
+
+function Database.query_return_value(self, sql_query)
+ local cursor = self.conn:execute(sql_query);
+
+ if cursor == nil and not self.conn:execute('SELECT @@VERSION') then
+ if self.log then
+ self.log:error('DATABASE_QUERY - lost connection: ', self.conn_id, ', environment: ', self.env, ', query: ', sql_query);
+ end
+ self:reconnect();
+ cursor = self.conn:execute(sql_query);
+ self.log:notice('DATABASE_QUERY - retry: ', sql_query);
+ end
+
+ if type(cursor) == 'userdata' then
+ local row = cursor:fetch({}, 'n');
+ cursor:close();
+
+ if not row then
+ return row;
+ else
+ return row[1];
+ end
+ end
+
+ return cursor;
+end
+
+
+function Database.last_insert_id(self)
+ return self:query_return_value('SELECT LAST_INSERT_ID()');
+end
+
+
+function Database.release(self, sql_query, call_function)
+ if self.conn then
+ self.conn:close();
+ end
+ if self.env then
+ self.env:close();
+ end
+
+ -- self.log:debug('DATABASE_RELEASE - connection: ', self.conn_id, ', status: ', self.env, ', ', self.conn);
+end
diff --git a/misc/freeswitch/scripts/common/fapi.lua b/misc/freeswitch/scripts/common/fapi.lua
new file mode 100644
index 0000000..0a05155
--- /dev/null
+++ b/misc/freeswitch/scripts/common/fapi.lua
@@ -0,0 +1,80 @@
+-- Gemeinschaft 5 module: FS api class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+FApi = {}
+
+-- create fapi object
+function FApi.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.class = 'fapi';
+ self.log = arg.log;
+ self.uuid = arg.uuid;
+ self.fs_api = freeswitch.API();
+ return object;
+end
+
+
+function FApi.return_result(self, result, positive, negative, unspecified)
+ if not result then
+ return negative;
+ end
+ result = tostring(result);
+
+ if result:match('^-ERR') then
+ return negative;
+ elseif result:match('^_undef_') then
+ return negative;
+ elseif result:match('^+OK') then
+ return positive;
+ else
+ return unspecified;
+ end
+end
+
+
+function FApi.sleep(self, value)
+ freeswitch.msleep(value);
+end
+
+
+function FApi.channel_exists(self, uuid)
+ require 'common.str'
+ uuid = uuid or self.uuid;
+ return common.str.to_b(freeswitch.API():execute('uuid_exists', tostring(uuid)));
+end
+
+
+function FApi.get_variable(self, variable_name)
+ local result = freeswitch.API():execute('uuid_getvar', tostring(self.uuid) .. ' ' .. tostring(variable_name));
+ return self:return_result(result, result, nil, result);
+end
+
+
+function FApi.set_variable(self, variable_name, value)
+ value = value or '';
+
+ local result = freeswitch.API():execute('uuid_setvar', tostring(self.uuid) .. ' ' .. tostring(variable_name) .. ' ' .. tostring(value));
+ return self:return_result(result, true);
+end
+
+
+function FApi.continue(self)
+ local result = freeswitch.API():execute('break', tostring(self.uuid));
+ return self:return_result(result, true, false);
+end
+
+function FApi.create_uuid(self, uuid)
+ local result = self.fs_api:execute('create_uuid', uuid);
+ return result;
+end
+
+function FApi.execute(self, function_name, function_parameters)
+ local result = self.fs_api:execute(function_name, function_parameters);
+ return self:return_result(result, true);
+end
diff --git a/misc/freeswitch/scripts/common/ipcalc.lua b/misc/freeswitch/scripts/common/ipcalc.lua
new file mode 100644
index 0000000..5c19d20
--- /dev/null
+++ b/misc/freeswitch/scripts/common/ipcalc.lua
@@ -0,0 +1,27 @@
+-- Gemeinschaft 5 module: ip calculation functions
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+function ipv4_to_i(ip_address_str)
+ local octet4, octet3, octet2, octet1 = ip_address_str:match('(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)');
+ if octet4 and octet3 and octet2 and octet1 then
+ return (2^24*octet4 + 2^16*octet3 + 2^8*octet2 + octet1);
+ end
+end
+
+function ipv4_to_network_netmask(ip_address_str)
+ local octet4, octet3, octet2, octet1, netmask = ip_address_str:match('(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)/(%d%d?)');
+ if octet4 and octet3 and octet2 and octet1 and netmask then
+ return (2^24*octet4 + 2^16*octet3 + 2^8*octet2 + octet1), tonumber(netmask);
+ end
+end
+
+function ipv4_network(ip_address, netmask)
+ return math.floor(ip_address / 2^(32-netmask));
+end
+
+function ipv4_in_network(ip_address, network, netmask)
+ return ipv4_network(ip_address, netmask) == ipv4_network(network, netmask);
+end
diff --git a/misc/freeswitch/scripts/common/log.lua b/misc/freeswitch/scripts/common/log.lua
new file mode 100644
index 0000000..d0d13dc
--- /dev/null
+++ b/misc/freeswitch/scripts/common/log.lua
@@ -0,0 +1,69 @@
+-- Gemeinschaft 5 module: log
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+Log = {}
+
+-- Create logger object
+function Log.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.prefix = arg.prefix or '### ';
+
+ self.level_console = arg.level_console or 0;
+ self.level_alert = arg.level_alert or 1;
+ self.level_critical = arg.level_critical or 2;
+ self.level_error = arg.level_error or 3;
+ self.level_warning = arg.level_warning or 4;
+ self.level_notice = arg.level_notice or 5;
+ self.level_info = arg.level_info or 6;
+ self.level_debug = arg.level_debug or 7;
+
+ return object;
+end
+
+function Log.message(self, log_level, message_arguments )
+ local message = tostring(self.prefix);
+ for index, value in pairs(message_arguments) do
+ if type(index) == 'number' then
+ message = message .. tostring(value);
+ end
+ end
+ freeswitch.consoleLog(log_level, message .. '\n');
+end
+
+function Log.console(self, ...)
+ self:message(self.level_console, arg);
+end
+
+function Log.alert(self, ...)
+ self:message(self.level_alert, arg);
+end
+
+function Log.critical(self, ...)
+ self:message(self.level_critical, arg);
+end
+
+function Log.error(self, ...)
+ self:message(self.level_error, arg);
+end
+
+function Log.warning(self, ...)
+ self:message(self.level_warning, arg);
+end
+
+function Log.notice(self, ...)
+ self:message(self.level_notice, arg);
+end
+
+function Log.info(self, ...)
+ self:message(self.level_info, arg);
+end
+
+function Log.debug(self, ...)
+ self:message(self.level_debug, arg);
+end
diff --git a/misc/freeswitch/scripts/common/node.lua b/misc/freeswitch/scripts/common/node.lua
new file mode 100644
index 0000000..544ede9
--- /dev/null
+++ b/misc/freeswitch/scripts/common/node.lua
@@ -0,0 +1,73 @@
+-- CommonModule: Node
+--
+module(...,package.seeall)
+
+Node = {}
+
+-- Create Node object
+function Node.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self)
+ self.__index = self
+ self.log = arg.log
+ self.database = arg.database
+ self.record = arg.record
+ self.session = arg.session
+ return object
+end
+
+-- Find Node account by name
+function Node.find_by_id(self, node_id)
+
+ if not tonumber(node_id) then
+ return nil
+ end
+
+ local sql_query = 'SELECT * FROM `gs_nodes` WHERE `id`= ' .. node_id .. ' LIMIT 1';
+ local record = nil
+
+ self.database:query(sql_query, function(node_entry)
+ record = node_entry
+ end)
+
+ if record then
+ local node_object = Node:new(self);
+ node_object.record = record
+
+ return node_object
+ end
+
+ return nil
+end
+
+-- Find Node account by name
+function Node.find_by_address(self, address)
+ local sql_query = 'SELECT * FROM `gs_nodes` WHERE `ip_address`= "' .. tostring(address):gsub('[^A-F0-9%.%:]', '') .. '" LIMIT 1';
+ local record = nil
+
+ self.database:query(sql_query, function(node_entry)
+ record = node_entry
+ end)
+
+ if record then
+ local node_object = Node:new(self);
+ node_object.record = record
+
+ return node_object
+ end
+
+ return nil
+end
+
+-- List Nodes
+function Node.all(self)
+ local sql_query = 'SELECT * FROM `gs_nodes`';
+ nodes = {};
+
+ self.database:query(sql_query, function(node_entry)
+ nodes[tonumber(node_entry.id)] = node_entry;
+ end)
+
+ return nodes
+end \ No newline at end of file
diff --git a/misc/freeswitch/scripts/common/phone_number.lua b/misc/freeswitch/scripts/common/phone_number.lua
new file mode 100644
index 0000000..f4f4bfe
--- /dev/null
+++ b/misc/freeswitch/scripts/common/phone_number.lua
@@ -0,0 +1,359 @@
+-- Gemeinschaft 5 module: phone number class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+PhoneNumber = {}
+
+PHONE_NUMBER_INTERNAL_TYPES = { 'SipAccount', 'Conference', 'FaxAccount', 'Callthrough', 'HuntGroup', 'AutomaticCallDistributor' }
+
+-- create phone number object
+function PhoneNumber.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.class = 'phonenumber';
+ self.log = arg.log;
+ self.database = arg.database;
+ self.record = arg.record;
+ self.domain = arg.domain;
+ self.DEFAULT_CALL_FORWARDING_DEPTH = 20;
+ return object;
+end
+
+-- find phone number by id
+function PhoneNumber.find_by_id(self, id)
+ local sql_query = 'SELECT * FROM `phone_numbers` WHERE `id`= ' .. tonumber(id) .. ' LIMIT 1';
+
+ local phone_number = nil;
+
+ self.database:query(sql_query, function(number_entry)
+ phone_number = PhoneNumber:new(self);
+ phone_number.record = number_entry;
+ phone_number.id = tonumber(number_entry.id);
+ phone_number.uuid = number_entry.uuid;
+ end)
+
+ return phone_number;
+end
+
+-- find phone number by number
+function PhoneNumber.find_by_number(self, number, phone_numberable_types)
+ require 'common.str'
+
+ phone_numberable_types = phone_numberable_types or PHONE_NUMBER_INTERNAL_TYPES
+
+ local sql_query = 'SELECT * FROM `phone_numbers` \
+ WHERE `number`= ' .. common.str.to_sql(number) .. ' \
+ AND `phone_numberable_type` IN ("' .. table.concat(phone_numberable_types, '","') .. '") \
+ AND `state` = "active" LIMIT 1';
+
+ local phone_number = nil;
+
+ self.database:query(sql_query, function(number_entry)
+ phone_number = PhoneNumber:new(self);
+ phone_number.record = number_entry;
+ end)
+
+ return phone_number;
+end
+
+-- Find numbers by owner id and type
+function PhoneNumber.find_all_by_owner(self, owner_id, owner_type)
+ local sql_query = 'SELECT * FROM `phone_numbers` WHERE `phone_numberable_type`="' .. owner_type .. '" AND `phone_numberable_id`= ' .. tonumber(owner_id) ..' ORDER BY `position`';
+ local phone_numbers = {}
+
+ self.database:query(sql_query, function(number_entry)
+ phone_numbers[tonumber(number_entry.id)] = PhoneNumber:new(self);
+ phone_numbers[tonumber(number_entry.id)].record = number_entry;
+ end)
+
+ return phone_numbers;
+end
+
+-- List numbers by owner id and type
+function PhoneNumber.list_by_owner(self, owner_id, owner_type)
+ local sql_query = 'SELECT * FROM `phone_numbers` WHERE `phone_numberable_type`="' .. owner_type .. '" AND `phone_numberable_id`= ' .. tonumber(owner_id) ..' ORDER BY `position`';
+ local phone_numbers = {}
+
+ self.database:query(sql_query, function(number_entry)
+ table.insert(phone_numbers, number_entry.number)
+ end)
+
+ return phone_numbers;
+end
+
+-- List numbers by same owner
+function PhoneNumber.list_by_same_owner(self, number, owner_types)
+ local phone_number = self:find_by_number(number, owner_types)
+
+ if phone_number then
+ return self:list_by_owner(phone_number.record.phone_numberable_id, phone_number.record.phone_numberable_type);
+ end
+end
+
+-- Retrieve call forwarding
+function PhoneNumber.call_forwarding(self, sources)
+ require 'common.str'
+
+ sources = sources or {};
+ table.insert(sources, '');
+
+ local sql_query = 'SELECT \
+ `a`.`destination` AS `number`, \
+ `a`.`call_forwardable_id` AS `id`, \
+ `a`.`call_forwardable_type` AS `type`, \
+ `a`.`timeout`, `a`.`depth`, \
+ `b`.`value` AS `service` \
+ FROM `call_forwards` `a` JOIN `call_forward_cases` `b` ON `a`.`call_forward_case_id` = `b`.`id` \
+ WHERE `a`.`phone_number_id`= ' .. tonumber(self.record.id) .. ' \
+ AND `a`.`active` IS TRUE \
+ AND (`a`.`source` IS NULL OR `a`.`source` IN ("' .. table.concat( sources, '","') .. '"))';
+
+ local call_forwarding = {}
+
+ self.database:query(sql_query, function(forwarding_entry)
+ call_forwarding[forwarding_entry.service] = forwarding_entry;
+ self.log:debug('CALL_FORWARDING_GET - PhoneNumber=', self.record.id, '/', self.record.uuid, '@', self.record.gs_node_id,
+ ', number: ', self.record.number,
+ ', service: ', forwarding_entry.service,
+ ', destination: ',forwarding_entry.type, '=', forwarding_entry.id,
+ ', number: ', forwarding_entry.number);
+ end)
+
+ return call_forwarding;
+end
+
+
+function PhoneNumber.call_forwarding_effective(self, service, source)
+ local conditions = {}
+ table.insert(conditions, '`phone_number_id` = ' .. self.record.id);
+
+ if source then
+ table.insert(conditions, '`source` = "' .. source);
+ else
+ table.insert(conditions, '(`source` = "" OR `source` IS NULL)');
+ end
+
+ if service then
+ table.insert(conditions, '`call_forward_case_id` IN (SELECT `id` FROM `call_forward_cases` WHERE `value` = "' .. service .. '")');
+ end
+
+ -- get call forwarding entry
+ local sql_query = 'SELECT `destination`,`active`,`timeout`,`call_forwardable_type`, `call_forwardable_id` FROM `call_forwards` WHERE ' .. table.concat(conditions, ' AND ') .. ' ORDER BY `active` DESC LIMIT 1';
+ local call_forwarding = nil;
+
+ self.database:query(sql_query, function(entry)
+ call_forwarding = entry;
+ end)
+
+ return call_forwarding;
+end
+
+
+function PhoneNumber.call_forwarding_off(self, service, source, delete)
+ local conditions = {}
+ table.insert(conditions, '`phone_number_id` = ' .. self.record.id);
+
+ if source then
+ table.insert(conditions, '`source` = "' .. source);
+ else
+ table.insert(conditions, '(`source` = "" OR `source` IS NULL)');
+ end
+
+ if service then
+ table.insert(conditions, '`call_forward_case_id` IN (SELECT `id` FROM `call_forward_cases` WHERE `value` = "' .. service .. '")');
+ end
+
+ self.log:info('PHONE_NUMBER_CALL_FORWARDING_OFF - service: ', service, ', number: ', self.record.number);
+
+ local call_forwarding_ids = {}
+
+ local sql_query = 'SELECT `id` FROM `call_forwards` WHERE ' .. table.concat(conditions, ' AND ');
+ self.database:query(sql_query, function(record)
+ table.insert(call_forwarding_ids, record.id);
+ end)
+
+ require 'common.call_forwarding'
+ local call_forwarding_class = common.call_forwarding.CallForwarding:new{ log = self.log, database = self.database, domain = self.domain };
+
+ for index, call_forwarding_id in ipairs(call_forwarding_ids) do
+ if tonumber(call_forwarding_id) then
+ local call_forwarding = call_forwarding_class:find_by_id(call_forwarding_id);
+ call_forwarding:presence_set('terminated');
+ end
+ end
+
+ -- set call forwarding entry inactive
+ local sql_query = 'UPDATE `call_forwards` SET `active` = FALSE, `updated_at` = NOW() WHERE ' .. table.concat(conditions, ' AND ');
+
+ local call_forwards = {};
+
+ -- or delete call forwarding entry
+ if delete then
+ sql_query = 'SELECT * FROM `call_forwards` WHERE ' .. table.concat(conditions, ' AND ');
+ self.database:query(sql_query, function(forwarding_entry)
+ table.insert(call_forwards, forwarding_entry)
+ end)
+ sql_query = 'DELETE FROM `call_forwards` WHERE ' .. table.concat(conditions, ' AND ');
+ end
+
+ if not self.database:query(sql_query) then
+ self.log:notice('PHONE_NUMBER_CALL_FORWARDING_OFF - call forwarding could not be deactivated - number: ', self.record.number);
+ return false;
+ end
+
+ if delete then
+ require 'common.sync_log'
+ local sync_log_class = common.sync_log.SyncLog:new{ log = self.log, database = self.database, homebase_ip_address = '' }
+
+ for index, call_forward in ipairs(call_forwards) do
+ sync_log_class:insert('CallForward', call_forward, 'destroy', nil);
+ end
+ end
+
+ return true;
+end
+
+
+function PhoneNumber.call_forwarding_on(self, service, destination, destination_type, timeout, source)
+ require 'common.str'
+ if call_forwarding_service == 'noanswer' then
+ timeout = tonumber(timeout) or '30';
+ else
+ timeout = 'NULL';
+ end
+
+ if source then
+ sql_query = 'SELECT `id`, `destination`, `call_forwardable_type`, `call_forward_case_id` FROM `call_forwards` \
+ WHERE `phone_number_id` = ' .. self.record.id .. ' \
+ AND `call_forward_case_id` IN (SELECT `id` FROM `call_forward_cases` WHERE `value` = "' .. service .. '") \
+ AND `source` = "' .. source .. '" AND `phone_number_id` = ' .. self.record.id .. ' ORDER BY `active` DESC LIMIT 1';
+ else
+ sql_query = 'SELECT `id`, `destination`, `call_forwardable_type`, `call_forward_case_id` FROM `call_forwards` \
+ WHERE `phone_number_id` = ' .. self.record.id .. ' \
+ AND `call_forward_case_id` IN (SELECT `id` FROM `call_forward_cases` WHERE `value` = "' .. service .. '") \
+ AND (`source` = "" OR `source` IS NULL) AND `phone_number_id` = ' .. self.record.id .. ' ORDER BY `active` DESC LIMIT 1';
+ end
+
+ destination_type = destination_type or '';
+ destination = destination or '';
+ local service_id = nil;
+ local entry_id = 'NULL';
+
+ self.database:query(sql_query, function(record)
+ entry_id = record.id;
+ service_id = record.call_forward_case_id;
+ if common.str.blank(destination) then
+ if not common.str.blank(record.call_forwardable_type) then
+ destination_type = common.str.downcase(record.call_forwardable_type);
+ end
+ if not common.str.blank(record.destination) then
+ destination = record.destination;
+ end
+ end
+ end)
+
+ if destination == '' and destination_type:lower() ~= 'voicemail' then
+ self.log:notice('PHONE_NUMBER_CALL_FORWARDING_ON - destination not specified - destination: ', destination, ', type: ', destination_type,', number: ' .. self.record.number);
+ return false;
+ end
+
+ if destination_type == '' then
+ destination_type = 'PhoneNumber';
+ end
+
+ self.log:info('PHONE_NUMBER_CALL_FORWARDING_ON - service: ', service, ', number: ', self.record.number, ', destination: ', destination, ', type: ', destination_type, ', timeout: ', timeout);
+
+ if not service_id then
+ sql_query = 'SELECT `id` FROM `call_forward_cases` WHERE `value` = "' .. service .. '"';
+ self.database:query(sql_query, function(record)
+ service_id = tonumber(record.id);
+ end);
+ end
+
+ sql_query = 'REPLACE INTO `call_forwards` \
+ (`active`, `uuid`, `depth`, `updated_at`, `id`, `phone_number_id`, `call_forward_case_id`, `destination`, `call_forwardable_type`, `timeout`) \
+ VALUES \
+ (TRUE, UUID(), ' .. self.DEFAULT_CALL_FORWARDING_DEPTH .. ', NOW(), ' .. entry_id .. ', ' .. self.record.id .. ', ' .. service_id .. ', "' .. destination .. '", "' .. destination_type .. '", ' .. timeout .. ')'
+
+ if not self.database:query(sql_query) then
+ self.log:error('PHONE_NUMBER_CALL_FORWARDING_ON - could not be activated - destination: ', destination, ', type: ', destination_type,', number: ' .. self.record.number);
+ return false;
+ end
+
+ require 'common.call_forwarding'
+ local call_forwarding_class = common.call_forwarding.CallForwarding:new{ log = self.log, database = self.database, domain = self.domain };
+ if tonumber(entry_id) then
+ local call_forwarding = call_forwarding_class:find_by_id(entry_id);
+ end
+
+ if call_forwarding then
+ if destination_type:lower() == 'voicemail' then
+ call_forwarding:presence_set('early');
+ else
+ call_forwarding:presence_set('confirmed');
+ end
+ end
+
+ return true;
+end
+
+
+function PhoneNumber.call_forwarding_toggle(self, service, source)
+ local call_forwarding = self:call_forwarding_effective(service, source);
+
+ -- no call_forwarding entry: all forwarding is deactivated
+ if not call_forwarding then
+ return false;
+ end
+
+ if tostring(call_forwarding.active) == '1' then
+ if self:call_forwarding_off(service, source) then
+ return {destination = call_forwarding.destination, destination_type = call_forwarding.destination_type, active = false};
+ end
+ end
+
+ if self:call_forwarding_on(service, call_forwarding.destination, call_forwarding.destination_type, call_forwarding.timeout, source) then
+ return {destination = call_forwarding.destination, destination_type = call_forwarding.destination_type, active = true};
+ end
+
+ return nil;
+end
+
+
+function PhoneNumber.call_forwarding_presence_set(self, presence_state, service)
+ service = service or 'always';
+ local dialplan_function = 'f-cfutg';
+
+ if service == 'assistant' then
+ dialplan_function = 'f-cfatg';
+ end
+
+ require "dialplan.presence"
+ local presence = dialplan.presence.Presence:new();
+
+ presence:init{log = self.log, accounts = { dialplan_function .. '-' .. tostring(self.record.id) }, domain = self.domain, uuid = 'call_forwarding_number_' .. tostring(self.record.id)};
+
+ return presence:set(presence_state);
+end
+
+
+-- Retrieve ringtone
+function PhoneNumber.ringtone(self, id)
+ id = id or self.record.id;
+ if not id then
+ return false;
+ end
+
+ local sql_query = "SELECT * FROM `ringtones` WHERE `ringtoneable_type` = \"PhoneNumber\" AND `ringtoneable_id`=" .. self.record.id .. " LIMIT 1";
+ local ringtone = nil;
+
+ self.database:query(sql_query, function(entry)
+ ringtone = entry;
+ end)
+
+ return ringtone;
+end
diff --git a/misc/freeswitch/scripts/common/routing_tables.lua b/misc/freeswitch/scripts/common/routing_tables.lua
new file mode 100644
index 0000000..34d0143
--- /dev/null
+++ b/misc/freeswitch/scripts/common/routing_tables.lua
@@ -0,0 +1,66 @@
+-- Gemeinschaft 5 module: routing table functions
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+function expand_variables(line, variables_list)
+ variables_list = variables_list or {};
+
+ return (line:gsub('{([%a%d_]+)}', function(captured)
+ return variables_list[captured] or '';
+ end))
+end
+
+
+function match_route(entry, search_str, variables_list)
+ if not entry or not search_str then
+ return { error = 'No input values' };
+ end
+
+ local result = nil;
+ local success = nil;
+ success, result = pcall(string.find, search_str, entry[1]);
+
+ if not success then
+ return { error = result, line = line }
+ elseif result then
+ local route = {
+ pattern = entry[1],
+ value = search_str:gsub(entry[1], expand_variables(entry[#entry], variables_list)),
+ }
+
+ for index = 2, #entry-1 do
+ local attribute = entry[index]:match('^(.-)%s*=');
+ if attribute then
+ route[attribute] = entry[index]:match('=%s*(.-)$');
+ end
+ end
+
+ return route;
+ end
+
+ return {};
+end
+
+
+function match_caller_id(entry, search_str, variables_list)
+ if not entry or not search_str then
+ return { error = 'No input values' };
+ end
+ local result = nil;
+ local success = nil;
+ success, result = pcall(string.find, search_str, entry[1]);
+ if not success then
+ return { error = result, line = line }
+ elseif result then
+ return {
+ value = search_str:gsub(entry[1], expand_variables(entry[4], variables_list)),
+ class = entry[2],
+ endpoint = entry[3],
+ pattern = entry[1],
+ }
+ end
+
+ return {};
+end
diff --git a/misc/freeswitch/scripts/common/sip_account.lua b/misc/freeswitch/scripts/common/sip_account.lua
new file mode 100644
index 0000000..28a00df
--- /dev/null
+++ b/misc/freeswitch/scripts/common/sip_account.lua
@@ -0,0 +1,137 @@
+-- Gemeinschaft 5 module: sip account class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+SipAccount = {}
+
+-- Create SipAccount object
+function SipAccount.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.class = 'sipaccount';
+ self.log = arg.log;
+ self.database = arg.database;
+ self.record = arg.record;
+ return object;
+end
+
+
+function SipAccount.find_by_sql(self, where)
+ local sql_query = 'SELECT \
+ `a`.`id`, \
+ `a`.`uuid`, \
+ `a`.`auth_name`, \
+ `a`.`caller_name`, \
+ `a`.`password`, \
+ `a`.`voicemail_pin`, \
+ `a`.`tenant_id`, \
+ `a`.`sip_domain_id`, \
+ `a`.`call_waiting`, \
+ `a`.`clir`, \
+ `a`.`clip`, \
+ `a`.`clip_no_screening`, \
+ `a`.`sip_accountable_type`, \
+ `a`.`sip_accountable_id`, \
+ `a`.`hotdeskable`, \
+ `a`.`gs_node_id`, \
+ `b`.`host` \
+ FROM `sip_accounts` `a` JOIN `sip_domains` `b` ON `a`.`sip_domain_id` = `b`.`id` \
+ WHERE ' .. where .. ' LIMIT 1';
+
+ local sip_account = nil;
+ self.database:query(sql_query, function(account_entry)
+ sip_account = SipAccount:new(self);
+ sip_account.record = account_entry;
+ sip_account.id = tonumber(account_entry.id);
+ sip_account.uuid = account_entry.uuid;
+ end)
+
+ return sip_account;
+end
+
+
+-- find sip account by id
+function SipAccount.find_by_id(self, id)
+ local sql_query = '`a`.`id`= ' .. tonumber(id);
+ return self:find_by_sql(sql_query);
+end
+
+-- find sip account by uuid
+function SipAccount.find_by_uuid(self, uuid)
+ local sql_query = '`a`.`uuid`= "' .. uuid .. '"';
+ return self:find_by_sql(sql_query);
+end
+
+-- Find SIP Account by auth_name
+function SipAccount.find_by_auth_name(self, auth_name, domain)
+ local sql_query = '`a`.`auth_name`= "' .. auth_name .. '"';
+
+ if domain then
+ sql_query = sql_query .. ' AND `b`.`host` = "' .. domain .. '"';
+ end
+
+ return self:find_by_sql(sql_query);
+end
+
+-- retrieve Phone Numbers for SIP Account
+function SipAccount.phone_numbers(self, id)
+ id = id or self.record.id;
+ if not id then
+ return false;
+ end
+
+ local sql_query = "SELECT * FROM `phone_numbers` WHERE `phone_numberable_type` = \"SipAccount\" AND `phone_numberable_id`=" .. self.record.id;
+ local phone_numbers = {}
+
+ self.database:query(sql_query, function(entry)
+ table.insert(phone_numbers,entry.number);
+ end)
+
+ return phone_numbers;
+end
+
+-- retrieve Ringtone for SIP Account
+function SipAccount.ringtone(self, id)
+ id = id or self.record.id;
+ if not id then
+ return false;
+ end
+
+ local sql_query = "SELECT * FROM `ringtones` WHERE `ringtoneable_type` = \"SipAccount\" AND `ringtoneable_id`=" .. self.record.id .. " LIMIT 1";
+ local ringtone = nil;
+
+ self.database:query(sql_query, function(entry)
+ ringtone = entry;
+ end)
+
+ return ringtone;
+end
+
+function SipAccount.send_text(self, text)
+ local event = freeswitch.Event("NOTIFY");
+ event:addHeader("profile", "gemeinschaft");
+ event:addHeader("event-string", "text");
+ event:addHeader("user", self.record.auth_name);
+ event:addHeader("host", self.record.host);
+ event:addHeader("content-type", "text/plain");
+ event:addBody(text);
+ event:fire();
+end
+
+
+function SipAccount.call_state(self)
+ local state = nil
+ local sql_query = "SELECT `callstate` FROM `channels` \
+ WHERE `name` LIKE (\"\%" .. self.record.auth_name .. "@%\") \
+ OR `name` LIKE (\"\%" .. self.record.auth_name .. "@%\") LIMIT 1";
+
+ self.database:query(sql_query, function(channel_entry)
+ state = channel_entry.callstate;
+ end)
+
+ return state;
+end
diff --git a/misc/freeswitch/scripts/common/str.lua b/misc/freeswitch/scripts/common/str.lua
new file mode 100644
index 0000000..b19f299
--- /dev/null
+++ b/misc/freeswitch/scripts/common/str.lua
@@ -0,0 +1,136 @@
+-- Gemeinschaft 5 module: string functions
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+function try(array, arguments)
+ local argument = arguments:match('^(.-)%.') or arguments;
+ local remaining_arguments = arguments:match('%.(.-)$');
+
+ if argument and type(array) == 'table' then
+ if remaining_arguments then
+ if type(array[argument]) == 'table' then
+ return try(array[argument], remaining_arguments);
+ else
+ return nil;
+ end
+ else
+ return array[argument];
+ end
+ end
+
+ return nil;
+end
+
+-- to number
+function to_n(value)
+ value = tostring(value):gsub('[^%d%.%+%-]', '');
+ return tonumber(value) or 0;
+end
+
+-- to integer
+function to_i(value)
+ return math.floor(to_n(value));
+end
+
+-- to string
+function to_s(value)
+ if value == nil then
+ return '';
+ end
+
+ return tostring(value);
+end
+
+-- to boolean
+function to_b(value)
+ if type(value) == 'boolean' then
+ return value;
+ elseif tonumber(value) then
+ return (tonumber(value) > 0);
+ else
+ return (tostring(value) == 'yes' or tostring(value) == 'true');
+ end
+end
+
+-- to array
+function to_a(line, separator)
+ line = line or '';
+ separator = separator or ';';
+ local result = {}
+ line:gsub('([^' .. separator .. ']+)', function(entry)
+ table.insert(result, entry);
+ end);
+
+ return result;
+end
+
+-- stripped to array
+function strip_to_a(line, separator)
+
+ local result = {}
+ line:gsub('([^' .. separator .. ']+)', function(entry)
+ table.insert(result, (entry:gsub('^%s+', ''):gsub('%s+$', '')));
+ end);
+
+ return result;
+end
+
+-- downcase
+function downcase(value)
+ if value == nil then
+ return '';
+ end
+
+ return tostring(value):lower();
+end
+
+-- remove special characters
+function to_ascii(value)
+ return (to_s(value):gsub('[^A-Za-z0-9%-%_ %(%)]', ''));
+end
+
+-- to SQL
+function to_sql(value)
+ if type(value) == 'boolean' then
+ return tostring(value):upper();
+ elseif type(value) == 'number' then
+ return tostring(value);
+ elseif type(value) == 'string' then
+ return '"' .. value:gsub('"', '\\"'):gsub("'", "\\'") .. '"';
+ else
+ return 'NULL';
+ end
+end
+
+-- to JSON
+function to_json(value)
+ if type(value) == 'boolean' then
+ return tostring(value):lower();
+ elseif type(value) == 'number' then
+ return tostring(value);
+ elseif type(value) == 'string' then
+ return '"' .. value:gsub('"', '\\"'):gsub("'", "\\'") .. '"';
+ else
+ return 'null';
+ end
+end
+
+-- remove leading/trailing whitespace
+function strip(value)
+ return (tostring(value):gsub('^%s+', ''):gsub('%s+$', ''));
+end
+
+-- split string
+function partition(value, separator)
+ value = tostring(value);
+ separator = separator or ':'
+
+ return value:match('^(.-)' .. separator), value:match(separator .. '(.-)$');
+end
+
+-- check if value is empty string or nil
+function blank(value)
+ return (value == nil or to_s(value) == '');
+end
diff --git a/misc/freeswitch/scripts/common/sync_log.lua b/misc/freeswitch/scripts/common/sync_log.lua
new file mode 100644
index 0000000..05b0dcf
--- /dev/null
+++ b/misc/freeswitch/scripts/common/sync_log.lua
@@ -0,0 +1,39 @@
+-- Gemeinschaft 5 module: sync log class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+SyncLog = {}
+
+-- create sync log object
+function SyncLog.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.log = arg.log;
+ self.database = arg.database;
+ self.homebase_ip_address = arg.homebase_ip_address;
+ return object;
+end
+
+-- create new entry
+function SyncLog.insert(self, entry_name, entry_record, action, history_entries)
+ local content = {}
+ for key, value in pairs(entry_record) do
+ require 'common.str'
+ table.insert(content, '"'.. key ..'":' .. common.str.to_json(value));
+ end
+
+ local history = '';
+ if action == 'update' then
+ history = 'Changed: ["' .. table.concat(history_entries, '","') .. '"]';
+ end
+
+ local sql_query = 'INSERT INTO `gs_cluster_sync_log_entries` (`waiting_to_be_synced`,`created_at`,`updated_at`,`class_name`,`action`,`content`,`history`,`homebase_ip_address`) \
+ VALUES \
+ (TRUE, NOW(), NOW(), \'' .. entry_name .. '\', \'' .. action .. '\', \'{' .. table.concat(content, ',') .. '}\', \'' .. history .. '\', \'' .. self.homebase_ip_address .. '\')';
+
+ return self.database:query(sql_query);
+end
diff --git a/misc/freeswitch/scripts/configuration.lua b/misc/freeswitch/scripts/configuration.lua
new file mode 100644
index 0000000..906d3f8
--- /dev/null
+++ b/misc/freeswitch/scripts/configuration.lua
@@ -0,0 +1,229 @@
+-- Gemeinschaft 5 dynamic freeswitch configuration
+-- (c) AMOOMA GmbH 2012
+--
+
+function nodes(database, local_node_id)
+ local gateways_xml = '';
+
+ require 'common.node'
+ for node_id, node_record in pairs(common.node.Node:new{log=log, database=database}:all()) do
+ if node_id ~= local_node_id then
+ local node_parameters = {}
+ node_parameters['username'] = node_record.name;
+ node_parameters['password'] = 'gemeinschaft';
+ node_parameters['proxy'] = node_record.ip_address;
+ node_parameters['register'] = 'false';
+ log:debug('NODE_GATEWAY ', node_record.id, ' - name: ', node_record.name, ', address: ', node_record.ip_address);
+ gateways_xml = gateways_xml .. xml:gateway(node_record.name, node_parameters);
+ end
+ end
+
+ return gateways_xml;
+end
+
+function gateways(profile_name)
+ local gateways_xml = '';
+ local gateways = common.configuration_file.get('/opt/freeswitch/scripts/ini/gateways.ini', false);
+
+ if not gateways then
+ return '';
+ end
+
+ for sofia_gateway, gateway_parameters in pairs(gateways) do
+ if tostring(gateway_parameters.profile) == profile_name then
+ log:debug('GATEWAY - name: ', sofia_gateway, ', address: ', gateway_parameters.proxy);
+ gateways_xml = gateways_xml .. xml:gateway(sofia_gateway, gateway_parameters);
+ end
+ end
+
+ return gateways_xml;
+end
+
+function profile(database, sofia_ini, profile_name, index, domains, node_id)
+ local profile_parameters = sofia_ini['profile:' .. profile_name];
+
+ if not profile_parameters then
+ log:error('SOFIA_PROFILE ', index,' - name: ', profile_name, ' - no parameters');
+ return '';
+ end
+ -- set local bind address
+ if domains[index] then
+ profile_parameters['sip-ip'] = domains[index]['host'];
+ profile_parameters['rtp-ip'] = domains[index]['host'];
+ profile_parameters['force-register-domain'] = domains[index]['host'];
+ profile_parameters['force-subscription-domain'] = domains[index]['host'];
+ profile_parameters['force-register-db-domain'] = domains[index]['host'];
+ log:debug('SOFIA_PROFILE ', index,' - name: ', profile_name, ', domain: ', domains[index]['host'], ', sip_bind: ', profile_parameters['sip-ip'], ':', profile_parameters['sip-port']);
+ else
+ log:error('SOFIA_PROFILE ', index,' - name: ', profile_name, ' - no domains');
+ end
+
+ local gateways_xml = gateways(profile_name);
+
+ if index == 1 then
+ gateways_xml = gateways_xml .. nodes(database, node_id);
+ end
+
+ return xml:sofia_profile(profile_name, profile_parameters, gateways_xml);
+end
+
+-- generate sofia.conf
+function conf_sofia(database)
+ local sofia_profile = "gemeinschaft";
+
+ require 'common.configuration_file'
+ local sofia_ini = common.configuration_file.get('/opt/freeswitch/scripts/ini/sofia.ini');
+ local dialplan_parameters = common.configuration_file.get('/opt/freeswitch/scripts/ini/dialplan.ini', 'parameters');
+
+ local local_node_id = tonumber(dialplan_parameters['node_id']) or 1;
+
+ require 'configuration.sip'
+ local domains = configuration.sip.Sip:new{ log = log, database = database}:domains();
+
+ sofia_profiles_xml = '';
+ for index, profile_name in ipairs(sofia_ini.profiles) do
+ sofia_profiles_xml = sofia_profiles_xml .. profile(database, sofia_ini, profile_name, index, domains, local_node_id);
+ end
+
+ XML_STRING = xml:document(xml:sofia(sofia_ini.parameters, sofia_profiles_xml))
+end
+
+function conf_conference(database)
+ XML_STRING = xml:document(xml:conference());
+
+ require 'common.configuration_file'
+ local conference_ini = common.configuration_file.get('/opt/freeswitch/scripts/ini/conferences.ini');
+ local conference_parameters = conference_ini.parameters;
+
+ local event_name = params:getHeader("Event-Name")
+ if event_name == 'COMMAND' then
+ local conf_name = params:getHeader('conf_name');
+ local profile_name = params:getHeader('profile_name');
+
+ if conf_name then
+ require 'common.conference'
+ conference = common.conference.Conference:new{log=log, database=database}:find_by_id(conf_name);
+ if conference then
+ log:debug('CONFIG_CONFERENCE ', conf_name, ' name: ', conference.record.name, ', profile: ', profile_name);
+ conference_parameters['caller-id-name'] = conference.record.name or '';
+ XML_STRING = xml:document(xml:conference(xml:conference_profile(profile_name, conference_parameters)));
+ else
+ log:error('CONFIG_CONFERENCE ', conf_name, ' - conference not found');
+ end
+ else
+ log:notice('CONFIG_CONFERENCE - no conference name');
+ end
+ else
+ log:debug('CONFIG_CONFERENCE ', conf_name, ' - event: ', event_name);
+ end
+end
+
+
+function directory_sip_account(database)
+ local key = params:getHeader('key');
+ local auth_name = params:getHeader('user');
+ local domain = params:getHeader('domain');
+ local purpose = params:getHeader('purpose');
+
+ if auth_name and auth_name ~= '' then
+ -- sip account or gateway
+ if string.len(auth_name) > 3 and auth_name:sub(1, 3) == 'gw+' then
+ local gateway_name = auth_name:sub(4);
+ domain = domain or freeswitch.API():execute('global_getvar', 'domain');
+ require 'configuration.sip'
+ log:notice('DATABASE: ', database);
+ local sip_gateway = configuration.sip.Sip:new{ log = log, database = database}:find_gateway_by_name(gateway_name);
+ if sip_gateway ~= nil and next(sip_gateway) ~= nil then
+ log:debug('DIRECTORY_GATEWAY - name: ', gateway_name, ', auth_name: ', auth_name);
+ XML_STRING = xml:document(xml:directory(xml:gateway_user(sip_gateway, gateway_name, auth_name), domain));
+ else
+ log:debug('DIRECTORY_GATEWAY - gateway not found - name: ', gateway_name, ', auth_name: ', auth_name);
+ end
+ else
+ require 'common.sip_account'
+ local sip_account = common.sip_account.SipAccount:new{ log = log, database = database}:find_by_auth_name(auth_name, domain);
+ if sip_account ~= nil then
+ if tostring(purpose) == 'publish-vm' then
+ log:debug('DIRECTORY_SIP_ACCOUNT - purpose: VoiceMail, auth_name: ', sip_account.record.auth_name, ', caller_name: ', sip_account.record.caller_name, ', domain: ', domain);
+ XML_STRING = xml:document(xml:directory(xml:group_default(xml:user(sip_account.record)), domain));
+ else
+ log:debug('DIRECTORY_SIP_ACCOUNT - auth_name: ', sip_account.record.auth_name, ', caller_name: ', sip_account.record.caller_name, ', domain: ', domain);
+ XML_STRING = xml:document(xml:directory(xml:user(sip_account.record), domain));
+ end
+ else
+ log:debug('DIRECTORY_SIP_ACCOUNT - sip account not found - auth_name: ', auth_name, ', domain: ', domain);
+ -- fake sip_account configuration
+ sip_account = {
+ auth_name = auth_name,
+ id = 0,
+ uuid = '',
+ password = tostring(math.random(0, 65534)),
+ voicemail_pin = '',
+ state = 'inactive',
+ caller_name = '',
+ sip_accountable_type = 'none',
+ sip_accountable_id = 0,
+ }
+ XML_STRING = xml:document(xml:directory(xml:user(sip_account), domain))
+ end
+ end
+ elseif tostring(XML_REQUEST.key_name) == 'name' and tostring(XML_REQUEST.key_value) ~= '' then
+ log:debug('DOMAIN_DIRECTORY - domain: ', XML_REQUEST.key_value);
+ XML_STRING = xml:document(xml:directory(nil, XML_REQUEST.key_value));
+ end
+end
+
+
+local log_identifier = XML_REQUEST.key_value or 'CONFIG';
+
+-- set logger
+require 'common.log'
+log = common.log.Log:new();
+log.prefix = '#C# [' .. log_identifier .. '] ';
+
+-- return a valid xml document
+require 'configuration.freeswitch_xml'
+xml = configuration.freeswitch_xml.FreeSwitchXml:new();
+XML_STRING = xml:document();
+
+local database = nil;
+
+-- log:debug('CONFIG_REQUEST section: ', XML_REQUEST.section, ', tag: ', XML_REQUEST.tag_name, ', key: ', XML_REQUEST.key_value);
+
+if XML_REQUEST.section == 'configuration' and XML_REQUEST.tag_name == 'configuration' then
+ -- database connection
+ require 'common.database'
+ database = common.database.Database:new{ log = log }:connect();
+ if database:connected() == false then
+ log:error('CONFIG_REQUEST - cannot connect to Gemeinschaft database');
+ return false;
+ end
+
+ if XML_REQUEST.key_value == 'sofia.conf' then
+ conf_sofia(database);
+ elseif XML_REQUEST.key_value == "conference.conf" then
+ conf_conference(database);
+ end
+elseif XML_REQUEST.section == 'directory' and XML_REQUEST.tag_name == '' then
+ log:debug('SIP_ACCOUNT_DIRECTORY - initialization phase');
+elseif XML_REQUEST.section == 'directory' and XML_REQUEST.tag_name == 'domain' then
+ if params == nil then
+ log:error('SIP_ACCOUNT_DIRECTORY - no parameters');
+ return false;
+ end
+
+ require 'common.database'
+ database = common.database.Database:new{ log = log }:connect();
+ if not database:connected() then
+ log:error('CONFIG_REQUEST - cannot connect to Gemeinschaft database');
+ return false;
+ end
+ directory_sip_account(database);
+else
+ log:error('CONFIG_REQUEST - no configuration handler, section: ', XML_REQUEST.section, ', tag: ', XML_REQUEST.tag_name);
+end
+
+-- ensure database handler is released on exit
+if database then
+ database:release();
+end
diff --git a/misc/freeswitch/scripts/configuration/freeswitch_xml.lua b/misc/freeswitch/scripts/configuration/freeswitch_xml.lua
new file mode 100644
index 0000000..c81bf50
--- /dev/null
+++ b/misc/freeswitch/scripts/configuration/freeswitch_xml.lua
@@ -0,0 +1,307 @@
+-- ConfigurationModule: FreeSwitchXml
+--
+module(...,package.seeall)
+
+FreeSwitchXml = {}
+
+-- Create FreeSwitchXml object
+function FreeSwitchXml.new(self, object)
+ object = object or {}
+ setmetatable(object, self)
+ self.__index = self
+ return object
+end
+
+function FreeSwitchXml.param(self, name, value)
+ return '<param name="' .. name .. '" value="' .. value .. '"/>'
+end
+
+function FreeSwitchXml.variable(self, name, value)
+ return '<variable name="' .. name .. '" value="' .. value .. '"/>'
+end
+
+function FreeSwitchXml.document(self, sections_xml)
+ if type(sections_xml) == "string" then
+ sections_xml = { sections_xml }
+ elseif type(sections_xml) == "nil" then
+ sections_xml = { "" }
+ end
+
+ local xml_string=
+[[<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="freeswitch/xml">
+]] .. table.concat(sections_xml, "\n") .. [[
+
+</document>]]
+
+ return xml_string
+end
+
+function FreeSwitchXml.directory(self, entries_xml, domain)
+ if type(entries_xml) == "string" then
+ entries_xml = { entries_xml }
+ elseif type(entries_xml) == "nil" then
+ entries_xml = { "" }
+ end
+
+ local xml_string =
+[[
+<section name="directory">
+<domain name="]] .. domain .. [[">
+<params>
+<param name="dial-string" value="${sofia_contact(${dialed_user}@${dialed_domain})}"/>
+</params>
+]] .. table.concat(entries_xml, "\n") .. [[
+
+</domain>
+</section>]]
+ return xml_string
+end
+
+function FreeSwitchXml.group_default(self, entries_xml)
+ if type(entries_xml) == "string" then
+ entries_xml = { entries_xml }
+ elseif type(entries_xml) == "nil" then
+ entries_xml = { "" }
+ end
+
+ local xml_string =
+[[
+<groups>
+<group name="default">
+<users>
+]] .. table.concat(entries_xml, "\n") .. [[
+
+</users>
+</group>
+</groups>]]
+ return xml_string
+end
+
+function FreeSwitchXml.user(self, user)
+ require 'common.configuration_file'
+ local params = common.configuration_file.get('/opt/freeswitch/scripts/ini/sip_accounts.ini', 'parameters');
+
+ params['password'] = user.password;
+ params['vm-password'] = user.voicemail_pin;
+
+ local variables = {
+ user_context = "default",
+ gs_from_gateway = "false",
+ gs_account_id = user.id,
+ gs_account_uuid = user.uuid,
+ gs_account_type = "SipAccount",
+ gs_account_state = user.state,
+ gs_account_caller_name = user.caller_name,
+ gs_account_owner_type = user.sip_accountable_type,
+ gs_account_owner_id = user.sip_accountable_id
+ }
+
+ local params_xml = {}
+ for name, value in pairs(params) do
+ params_xml[#params_xml+1] = self:param(name, value)
+ end
+
+ local variables_xml = {}
+ for name, value in pairs(variables) do
+ variables_xml[#variables_xml+1] = self:variable(name, value)
+ end
+
+ local xml_string =
+[[
+<user id="]] .. user.auth_name .. [[">
+<params>
+]] .. table.concat(params_xml, "\n") .. [[
+
+</params>
+<variables>
+]] .. table.concat(variables_xml, "\n") .. [[
+
+</variables>
+</user>]]
+ return xml_string
+end
+
+function FreeSwitchXml.gateway_user(self, user, gateway_name, auth_name)
+ user.id = user.id or 0
+
+ local params = {
+ ['password'] = user.password,
+ }
+
+ local variables = {
+ user_context = "default",
+ gs_from_gateway = "true",
+ gs_gateway_name = gateway_name,
+ gs_gateway_id = user.id
+ }
+
+ local params_xml = {}
+ for name, value in pairs(params) do
+ params_xml[#params_xml+1] = self:param(name, value)
+ end
+
+ local variables_xml = {}
+ for name, value in pairs(variables) do
+ variables_xml[#variables_xml+1] = self:variable(name, value)
+ end
+
+ local xml_string =
+[[
+<user id="]] .. auth_name .. [[">
+<params>
+]] .. table.concat(params_xml, "\n") .. [[
+
+</params>
+<variables>
+]] .. table.concat(variables_xml, "\n") .. [[
+
+</variables>
+</user>]]
+ return xml_string
+end
+
+function FreeSwitchXml.sofia(self, parameters, profiles_xml)
+ if type(profiles_xml) == "string" then
+ profiles_xml = { profiles_xml }
+ elseif type(profiles_xml) == "nil" then
+ profiles_xml = { "" }
+ end
+
+ local params_xml = {}
+ for name, value in pairs(parameters) do
+ params_xml[#params_xml+1] = self:param(name, value)
+ end
+
+ local xml_string =
+[[
+<section name="configuration" description="FreeSwitch configuration for Sofia Profile">
+<configuration name="sofia.conf" description="Sofia SIP Configuration">
+<global_settings>
+]] .. table.concat(params_xml, "\n") .. [[
+
+</global_settings>
+<profiles>
+]] .. table.concat(profiles_xml, "\n") .. [[
+
+</profiles>
+</configuration>
+</section>]]
+ return xml_string
+end
+
+function FreeSwitchXml.sofia_profile(self, profile_name, parameters, gateways_xml)
+ params_xml = {}
+ for name, value in pairs(parameters) do
+ params_xml[#params_xml+1] = self:param(name, value)
+ end
+
+ if type(gateways_xml) == "string" then
+ gateways_xml = { gateways_xml }
+ elseif type(gateways_xml) == "nil" then
+ gateways_xml = { "" }
+ end
+
+ local xml_string =
+[[
+<profile name="]] .. profile_name .. [[">
+<aliases>
+</aliases>
+<gateways>
+]] .. table.concat(gateways_xml, "\n") .. [[
+
+</gateways>
+<domains>
+<domain name="all" alias="true" parse="false"/>
+</domains>
+<settings>
+]] .. table.concat(params_xml, "\n") .. [[
+
+</settings>
+</profile>]]
+ return xml_string
+end
+
+function FreeSwitchXml.gateway(self, gateway_name, parameters)
+ local params_xml = {}
+ if parameters then
+ for name, value in pairs(parameters) do
+ params_xml[#params_xml+1] = self:param(name, value)
+ end
+ end
+
+ local xml_string =
+[[
+<gateway name="]] .. gateway_name .. [[">
+]] .. table.concat(params_xml, "\n") .. [[
+
+</gateway>]]
+ return xml_string
+end
+
+function FreeSwitchXml.conference(self, profiles_xml)
+ if type(profiles_xml) == "string" then
+ profiles_xml = { profiles_xml }
+ elseif type(profiles_xml) == "nil" then
+ profiles_xml = { "" }
+ end
+
+ local xml_string =
+[[
+<section name="configuration" description="FreeSwitch configuration for Sofia Profile">
+<configuration name="conference.conf" description="Conference configuration">
+<advertise>
+</advertise>
+<caller-controls>
+<group name="speaker">
+<control action="mute"/>
+<control action="deaf mute" digits="*"/>
+<control action="energy up" digits="9"/>
+<control action="energy equ" digits="8"/>
+<control action="energy dn" digits="7"/>
+<control action="vol talk up" digits="3"/>
+<control action="vol talk zero" digits="2"/>
+<control action="vol talk dn" digits="1"/>
+<control action="vol listen up" digits="6"/>
+<control action="vol listen zero" digits="5"/>
+<control action="vol listen dn" digits="4"/>
+<control action="hangup" digits="#"/>
+</group>
+<group name="moderator">
+<control action="mute" digits="0"/>
+<control action="deaf mute" digits="*"/>
+<control action="energy up" digits="9"/>
+<control action="energy equ" digits="8"/>
+<control action="energy dn" digits="7"/>
+<control action="vol talk up" digits="3"/>
+<control action="vol talk zero" digits="2"/>
+<control action="vol talk dn" digits="1"/>
+<control action="vol listen up" digits="6"/>
+<control action="vol listen zero" digits="5"/>
+<control action="vol listen dn" digits="4"/>
+<control action="hangup" digits="#"/>
+</group>
+</caller-controls>
+<profiles>
+]] .. table.concat(profiles_xml, "\n") .. [[
+
+</profiles>
+</configuration>
+</section>]]
+ return xml_string
+end
+
+function FreeSwitchXml.conference_profile(self, profile_name, parameters)
+ params_xml = {}
+ for name, value in pairs(parameters) do
+ params_xml[#params_xml+1] = self:param(name, value)
+ end
+
+ local xml_string =
+[[
+<profile name="]] .. profile_name .. [[">
+]] .. table.concat(params_xml, "\n") .. [[
+
+</profile>]]
+ return xml_string
+end
diff --git a/misc/freeswitch/scripts/configuration/sip.lua b/misc/freeswitch/scripts/configuration/sip.lua
new file mode 100644
index 0000000..78143bc
--- /dev/null
+++ b/misc/freeswitch/scripts/configuration/sip.lua
@@ -0,0 +1,37 @@
+-- Gemeinschaft 5 module: sip configuration class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+Sip = {}
+
+-- create sip configuration object
+function Sip.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.log = arg.log;
+ self.database = arg.database;
+ self.record = arg.record;
+ return object;
+end
+
+-- find gateway by name
+function Sip.find_gateway_by_name(self, name)
+ require 'common.configuration_file'
+ return common.configuration_file.get('/opt/freeswitch/scripts/ini/gateways.ini', name);
+end
+
+-- list sip domains
+function Sip.domains(self)
+ local sql_query = 'SELECT * FROM `sip_domains`';
+ local sip_domains = {}
+
+ self.database:query(sql_query, function(sip_domain)
+ table.insert(sip_domains, sip_domain);
+ end)
+
+ return sip_domains;
+end
diff --git a/misc/freeswitch/scripts/dialplan/access_authorizations.lua b/misc/freeswitch/scripts/dialplan/access_authorizations.lua
new file mode 100644
index 0000000..dbacf20
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/access_authorizations.lua
@@ -0,0 +1,52 @@
+-- CommonModule: AccessAuthorization
+--
+module(...,package.seeall)
+
+AccessAuthorization = {}
+
+-- Create AccessAuthorization object
+function AccessAuthorization.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self)
+ self.__index = self
+ self.log = arg.log
+ self.database = arg.database
+ self.record = arg.record
+ self.session = arg.session
+ return object
+end
+
+-- Find AccessAuthorization by ID
+function AccessAuthorization.find_by_id(self, id)
+ local sql_query = string.format("SELECT * FROM `access_authorizations` WHERE `id`=%d LIMIT 1", id)
+ local record = nil
+
+ self.database:query(sql_query, function(access_authorization_entry)
+ record = access_authorization_entry
+ end)
+
+ if record then
+ access_authorization = AccessAuthorization:new(self)
+ access_authorization.record = record
+ return access_authorization
+ end
+
+ return nil
+end
+
+-- list accessauthorization by owner
+function AccessAuthorization.list_by_owner(self, owner_id, owner_type)
+ local sql_query = 'SELECT `a`.`id`, `a`.`name`, `a`.`login`, `a`.`pin`, `a`.`sip_account_id`, `b`.`number` AS `phone_number` \
+ FROM `access_authorizations` `a` \
+ LEFT JOIN `phone_numbers` `b` ON `b`.`phone_numberable_id` = `a`.`id` AND `b`.`phone_numberable_type` = "AccessAuthorization" \
+ WHERE `a`.`access_authorizationable_type` = "' .. owner_type .. '" AND `access_authorizationable_id`= ' .. tonumber(owner_id);
+
+ local access_authorizations = {}
+
+ self.database:query(sql_query, function(access_authorization_entry)
+ table.insert(access_authorizations, access_authorization_entry);
+ end);
+
+ return access_authorizations;
+end
diff --git a/misc/freeswitch/scripts/dialplan/acd.lua b/misc/freeswitch/scripts/dialplan/acd.lua
new file mode 100644
index 0000000..563d836
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/acd.lua
@@ -0,0 +1,484 @@
+-- Gemeinschaft 5 module: acd class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+AutomaticCallDistributor = {}
+
+local DEFAULT_AGENT_TIMEOUT = 20;
+local DEFAULT_TIME_RES = 5;
+local DEFAULT_WAIT_TIMEOUT = 360;
+local DEFAULT_RETRY_TIME = 2;
+local DEFAULT_MUSIC_ON_WAIT = 'tone_stream://%(2000,4000,440.0,480.0);loops=-1';
+
+-- create acd object
+function AutomaticCallDistributor.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.class = 'automaticcalldistributor';
+ self.log = arg.log;
+ self.database = arg.database;
+ self.record = arg.record;
+ self.acd_caller_id = arg.acd_caller_id;
+ self.domain = arg.domain;
+ return object;
+end
+
+
+function AutomaticCallDistributor.find_by_sql(self, sql_query)
+ local acd = nil;
+
+ require 'common.str'
+
+ self.database:query(sql_query, function(entry)
+ acd = AutomaticCallDistributor:new(self);
+ acd.record = entry;
+ acd.id = tonumber(entry.id);
+ acd.uuid = entry.uuid;
+ acd.agent_timeout = tonumber(entry.agent_timeout) or DEFAULT_AGENT_TIMEOUT;
+ acd.announce_position = tonumber(entry.announce_position);
+ acd.announce_call_agents = common.str.to_s(entry.announce_call_agents);
+ acd.greeting = common.str.to_s(entry.greeting);
+ acd.goodbye = common.str.to_s(entry.goodbye);
+ acd.music = common.str.to_s(entry.music);
+ acd.strategy = common.str.to_s(entry.strategy);
+ acd.join = common.str.to_s(entry.join);
+ acd.leave = common.str.to_s(entry.leave);
+ end)
+
+ return acd;
+end
+
+
+function AutomaticCallDistributor.find_by_id(self, id)
+ local sql_query = 'SELECT * FROM `automatic_call_distributors` WHERE `id`= '.. tonumber(id) .. ' LIMIT 1';
+ return self:find_by_sql(sql_query);
+end
+
+
+function AutomaticCallDistributor.find_by_uuid(self, uuid)
+ local sql_query = 'SELECT * FROM `automatic_call_distributors` WHERE `uuid`= "'.. tostring(uuid) .. '" LIMIT 1';
+ return self:find_by_sql(sql_query);
+end
+
+
+function AutomaticCallDistributor.callers_count(self)
+ return self.database:query_return_value('SELECT COUNT(*) FROM `acd_callers` `a` JOIN `channels` `b` ON `a`.`channel_uuid` = `b`.`uuid` WHERE `automatic_call_distributor_id` = ' .. self.id);
+end
+
+
+function AutomaticCallDistributor.caller_new(self, uuid)
+ local sql_query = 'INSERT INTO `acd_callers` \
+ (`enter_time`, `created_at`, `updated_at`, `status`, `automatic_call_distributor_id`, `channel_uuid`) \
+ VALUES (NOW(), NOW(), NOW(), "enter", ' .. self.id .. ', "' .. uuid .. '")';
+
+ if self.database:query(sql_query) then
+ self.acd_caller_id = self.database:last_insert_id();
+ end
+end
+
+
+function AutomaticCallDistributor.caller_update(self, attributes)
+ local attributes_sql = { '`updated_at` = NOW()' };
+ for key, value in pairs(attributes) do
+ table.insert(attributes_sql, '`' .. key .. '` = "' .. value .. '"');
+ end
+
+ local sql_query = 'UPDATE `acd_callers` \
+ SET '.. table.concat(attributes_sql, ',') .. '\
+ WHERE `id` = ' .. tonumber(self.acd_caller_id);
+ return self.database:query(sql_query);
+end
+
+
+function AutomaticCallDistributor.caller_delete(self, id)
+ id = id or self.acd_caller_id;
+ local sql_query = 'DELETE FROM `acd_callers` \
+ WHERE `id` = ' .. tonumber(id);
+ return self.database:query(sql_query);
+end
+
+
+function AutomaticCallDistributor.agent_find_by_acd_and_destination(self, acd_id, destination_type, destination_id)
+ local sql_query = 'SELECT * FROM `acd_agents` \
+ WHERE `automatic_call_distributor_id` = ' .. acd_id .. ' \
+ AND `destination_type` = "' .. destination_type .. '" \
+ AND `destination_id` = ' .. destination_id;
+
+ local agent = nil;
+ self.database:query(sql_query, function(entry)
+ agent = entry;
+ end)
+
+ return agent;
+end
+
+
+function AutomaticCallDistributor.agent_status_presence_set(self, agent_id, presence_state)
+ require "dialplan.presence"
+ local presence = dialplan.presence.Presence:new();
+
+ presence:init{log = self.log, accounts = { 'f-acdmtg-' .. tostring(agent_id) }, domain = self.domain, uuid = 'acd_agent_' .. tostring(agent_id)};
+ return presence:set(presence_state);
+end
+
+
+function AutomaticCallDistributor.agent_status_get(self, agent_id)
+ local sql_query = 'SELECT `status` FROM `acd_agents` WHERE `id` = ' .. agent_id;
+ return self.database:query_return_value(sql_query);
+end
+
+
+function AutomaticCallDistributor.agent_status_toggle(self, agent_id, destination_type, destination_id)
+ local sql_query = 'UPDATE `acd_agents` SET `status` = IF(`status` = "active", "inactive", "active") \
+ WHERE `id` = ' .. agent_id .. ' \
+ AND `destination_type` = "' .. destination_type .. '" \
+ AND `destination_id` = ' .. destination_id;
+
+ if not self.database:query(sql_query) then
+ return nil;
+ end
+
+ local status = self:agent_status_get(agent_id);
+
+ if tostring(status) == 'active' then
+ self:agent_status_presence_set(agent_id, 'confirmed');
+ else
+ self:agent_status_presence_set(agent_id, 'terminated');
+ end
+
+ return status;
+end
+
+
+function AutomaticCallDistributor.agents_active(self)
+ local sql_query = 'SELECT * FROM `acd_agents` \
+ WHERE `status` = "active" AND destination_type != "SipAccount" AND `automatic_call_distributor_id` = ' .. tonumber(self.id);
+
+ local agents = {}
+ self.database:query(sql_query, function(entry)
+ table.insert(agents, entry);
+ end);
+
+ local sql_query = 'SELECT `a`.* FROM `acd_agents` `a` \
+ JOIN `sip_accounts` `b` ON `a`.`destination_id` = `b`.`id` \
+ JOIN `sip_registrations` `c` ON `b`.`auth_name` = `c`.`sip_user` \
+ WHERE `a`.`status` = "active" AND `a`.destination_type = "SipAccount" AND `a`.`automatic_call_distributor_id` = ' .. tonumber(self.id);
+
+ self.database:query(sql_query, function(entry)
+ table.insert(agents, entry);
+ end);
+
+ return agents;
+end
+
+
+function AutomaticCallDistributor.agents_available(self, strategy)
+ local order_by = '`a`.`id` DESC';
+
+ if strategy then
+ if strategy == 'round_robin' then
+ order_by = '`a`.`last_call` ASC, `a`.`id` DESC';
+ end
+ end
+
+ local sql_query = 'SELECT `a`.`id`, `a`.`name`, `a`.`destination_type`, `a`.`destination_id`, `b`.`auth_name`, `b`.`gs_node_id`, `c`.`callstate` \
+ FROM `acd_agents` `a` LEFT JOIN `sip_accounts` `b` ON `a`.`destination_id` = `b`.`id` \
+ JOIN `sip_registrations` `d` ON `b`.`auth_name` = `d`.`sip_user` \
+ LEFT JOIN `channels` `c` ON `c`.`name` LIKE CONCAT("%", `b`.`auth_name`, "@%") \
+ WHERE `a`.`status` = "active" AND `a`.`destination_id` IS NOT NULL AND `a`.`automatic_call_distributor_id` = ' .. tonumber(self.id) .. ' \
+ ORDER BY ' .. order_by;
+
+ local accounts = {}
+ self.database:query(sql_query, function(entry)
+ if not entry.callstate then
+ table.insert(accounts, entry);
+ end
+ end);
+
+ return accounts;
+end
+
+
+function AutomaticCallDistributor.agent_update_call(self, agent_id)
+
+ local sql_query = 'UPDATE `acd_agents` \
+ SET `last_call` = NOW(), `calls_answered` = IFNULL(`calls_answered`, 0) + 1 \
+ WHERE `id` = ' .. tonumber(agent_id);
+ return self.database:query(sql_query);
+end
+
+
+function AutomaticCallDistributor.call_position(self)
+ local sql_query = 'SELECT COUNT(*) FROM `acd_callers` `a` JOIN `channels` `b` ON `a`.`channel_uuid` = `b`.`uuid` \
+ WHERE `automatic_call_distributor_id` = ' .. tonumber(self.id) .. ' AND `status` = "waiting" AND `id` < ' .. tonumber(self.acd_caller_id);
+
+ return tonumber(self.database:query_return_value(sql_query));
+end
+
+
+function AutomaticCallDistributor.wait_turn(self, caller_uuid, acd_caller_id, timeout, retry_timeout)
+ self.acd_caller_id = acd_caller_id or self.acd_caller_id;
+ timeout = timeout or DEFAULT_WAIT_TIMEOUT;
+ local available_agents = {};
+ local active_agents = {};
+ local position = self:call_position();
+
+ self.log:info('ACD ', self.id, ' WAIT - timeout: ', timeout, ', res: ', DEFAULT_TIME_RES, ', retry_timeout: ', retry_timeout, ', position: ', position + 1);
+
+ require 'common.fapi'
+ local fapi = common.fapi.FApi:new{ log = self.log, uuid = caller_uuid }
+
+ local acd_status = nil;
+ local start_time = os.time();
+ local exit_time = start_time + timeout;
+
+ if tonumber(retry_timeout) then
+ self.log:info('ACD ', self.id, ' WAIT - retry_timeout: ', retry_timeout);
+ fapi:sleep(retry_timeout * 1000);
+ end
+
+ while (exit_time > os.time() and fapi:channel_exists()) do
+ available_agents = self:agents_available();
+ active_agents = self:agents_active();
+ local current_position = self:call_position();
+
+ if position ~= current_position then
+ position = current_position;
+ self.log:info('ACD ', self.id, ' WAIT - agents: ', #available_agents, '/', #active_agents, ', position: ', position + 1, ', wait_time: ', os.time()-start_time);
+
+ if tostring(self.announce_position) >= '0' and position > 0 then
+ acd_status = 'announce_position';
+ fapi:set_variable('acd_position', position + 1);
+ break;
+ end
+ else
+ self.log:debug('ACD ', self.id, ' WAIT - agents: ', #available_agents, '/', #active_agents, ', position: ', position + 1, ', wait_time: ', os.time()-start_time);
+ end
+
+ if #available_agents == 0 and self.leave:find('no_agents_available') then
+ acd_status = 'no_agents';
+ break;
+ elseif #active_agents == 0 and self.leave:find('no_agents_active') then
+ acd_status = 'no_agents';
+ break;
+ elseif position == 0 and #available_agents > 0 then
+ acd_status = 'call_agents';
+ break;
+ end
+
+ if tonumber(self.announce_position) and tonumber(self.announce_position) > 0 and tonumber(self.announce_position) <= os.time()-start_time then
+ acd_status = 'announce_position';
+ fapi:set_variable('acd_position', position + 1);
+ break;
+ end
+
+ fapi:sleep(DEFAULT_TIME_RES * 1000);
+ end
+
+ if not acd_status then
+ if (exit_time <= os.time()) then
+ acd_status = 'timeout';
+ else
+ acd_status = 'unspecified';
+ end
+ end
+
+ self.log:info('ACD ', self.id, ' WAIT END - status: ', acd_status, ', agents: ', #available_agents, '/', #active_agents, ', position: ', position + 1, ', wait_time: ', os.time()-start_time);
+
+ fapi:set_variable('acd_status', acd_status);
+ if tostring(fapi:get_variable('acd_waiting')) == 'true' then
+ fapi:continue();
+ end
+end
+
+
+function AutomaticCallDistributor.wait_play_music(self, caller, timeout, retry_timeout, music)
+ local result = caller:result('luarun(acd_wait.lua ' .. caller.uuid .. ' ' .. tostring(self.id) .. ' ' .. tostring(timeout) .. ' ' .. tostring(retry_timeout) .. ' ' .. self.acd_caller_id .. ')');
+ if not tostring(result):match('^+OK') then
+ self.log:error('ACD ', self.id,' WAIT_PLAY_MUSIC - error starting acd thread');
+ return 'error';
+ end
+
+ caller:set_variable('acd_waiting', true);
+ caller.session:streamFile(music or DEFAULT_MUSIC_ON_WAIT);
+ caller:set_variable('acd_waiting', false);
+
+ local acd_status = caller:to_s('acd_status');
+ if acd_status == '' then
+ acd_status = 'abandoned';
+ end
+
+ return acd_status;
+end
+
+
+function AutomaticCallDistributor.on_answer(self, destination)
+ self.log:info('ACD ', self.id, ' ANSWERED - agent: ', destination.type, '=', destination.id, '/', destination.uuid)
+ self:caller_update({status = 'answered'});
+end
+
+
+function AutomaticCallDistributor.call_agents(self, dialplan_object, caller, destination)
+ local available_agents = self:agents_available(self.strategy);
+
+ self.log:info('ACD ', self.id, ' CALL_AGENTS - strategy: ', self.strategy, ', available_agents: ', #available_agents);
+
+ caller:set_variable('ring_ready', true);
+
+ local destinations = {}
+ for index, agent in ipairs(available_agents) do
+ self.log:info('ACD ', self.id, ' AGENT - name: ', agent.name, ', destination: ', agent.destination_type, '=', agent.destination_id, '@', agent.gs_node_id, ', local_node: ', dialplan_object.node_id);
+ table.insert(destinations, dialplan_object:destination_new{ type = agent.destination_type, id = agent.destination_id, node_id = agent.gs_node_id, data = agent.id });
+ end
+
+ local result = { continue = false };
+ local start_time = os.time();
+
+ require 'dialplan.sip_call'
+ if self.strategy == 'ring_all' then
+ result = dialplan.sip_call.SipCall:new{ log = self.log, database = self.database, caller = caller, calling_object = self, on_answer = self.on_answer }:fork(destinations,
+ {
+ callee_id_number = destination.number,
+ timeout = self.agent_timeout,
+ send_ringing = ( dialplan_object.send_ringing_to_gateways and caller.from_gateway ),
+ });
+ self.log:info('ACD ', self.id, ' CALL_AGENTS - success, fork_index: ', result.fork_index);
+ if result.fork_index then
+ result.destination = destinations[result.fork_index];
+ end
+ return result;
+ else
+ for index, destination in ipairs(destinations) do
+ if os.time() > (self.start_time + self.timeout) and caller.session:ready() then
+ self.log:info('ACD ', self.id, ' CALL_AGENTS - timeout');
+ return { disposition = 'ACD_TIMEOUT', code = 480, phrase = 'Timeout' }
+ end
+
+ self.log:info('ACD ', self.id, ' CALL_AGENT - ', destination.type, '=', destination.id, ', timeout: ', self.agent_timeout);
+ result = dialplan.sip_call.SipCall:new{ log = self.log, database = self.database, caller = caller, calling_object = self, on_answer = self.on_answer }:fork({ destination },
+ {
+ callee_id_number = destination.number,
+ timeout = self.agent_timeout,
+ send_ringing = ( dialplan_object.send_ringing_to_gateways and caller.from_gateway ),
+ });
+ if result.disposition == 'SUCCESS' then
+ self.log:info('ACD ', self.id, ' CALL_AGENTS - success, agent_id: ', destination.data);
+ self:agent_update_call(destination.data);
+ result.destination = destination;
+ return result;
+ end
+ end
+ end
+
+ return { disposition = 'ACD_NO_AGENTS', code = 480, phrase = 'No active agents' }
+end
+
+
+function AutomaticCallDistributor.run(self, dialplan_object, caller, destination)
+ require 'common.str'
+
+ local callers_count = self:callers_count();
+ local active_agents = self:agents_active();
+ local available_agents = self:agents_available();
+ local position = self:call_position();
+
+ if self.leave:find('timeout') then
+ self.timeout = dialplan_object.dial_timeout_active;
+ else
+ self.timeout = 86400;
+ end
+
+ self.log:info('ACD ', self.id,' - ', self.class, '=', self.id, '/', self.uuid, ', acd_caller=', self.acd_caller_id, ', callers: ', callers_count, ', agents: ', #available_agents, '/', #active_agents, ', position: ', position + 1, ', music: ', tostring(self.music));
+
+ if self.join == 'agents_active' and #active_agents == 0 then
+ self.log:info('ACD ', self.id, ' - no agents active');
+ return { disposition = 'ACD_NO_AGENTS', code = 480, phrase = 'No agents' }
+ end
+
+ if self.join == 'agents_available' and #available_agents == 0 then
+ self.log:info('ACD ', self.id, ' - no agents available');
+ return { disposition = 'ACD_NO_AGENTS', code = 480, phrase = 'All agents busy' }
+ end
+
+ if not common.str.blank(self.music) then
+ caller:set_variable('ringback', self.music);
+ else
+ self.music = false;
+ end
+
+ if self.music then
+ caller.session:answer();
+ else
+ caller:set_variable('instant_ringback', true);
+ end
+
+ self.start_time = os.time();
+ caller:sleep(500);
+ local acd_status = 'waiting';
+ self:caller_update({status = acd_status});
+
+ local retry_timeout = nil;
+ local result = { disposition = 'ACD_NO_AGENTS', code = 480, phrase = 'No active agents' }
+
+ if self.greeting then
+ caller.session:sayPhrase('acd_greeting', self.greeting);
+ end
+
+ if self.announce_position then
+ local current_position = self:call_position();
+ if tonumber(current_position) then
+ caller.session:sayPhrase('acd_announce_position_enter', tonumber(current_position) + 1);
+ end
+ end
+
+ while acd_status == 'waiting' and caller.session:ready() do
+ acd_status = self:wait_play_music(caller, self.timeout - (os.time() - self.start_time), retry_timeout, self.music);
+ self.log:info('ACD ', self.id, ' PROCESS - status: ', acd_status, ', wait_time: ', (os.time() - self.start_time));
+
+ if not caller.session:ready() then
+ acd_status = 'abandoned';
+ break;
+ elseif os.time() >= (self.start_time + self.timeout) then
+ acd_status = 'timeout';
+ break;
+ elseif acd_status == 'no_agents' then
+ break;
+ elseif acd_status == 'call_agents' then
+ if self.announce_call_agents ~= '' then
+ caller.session:sayPhrase('acd_announce_call_agents', self.announce_call_agents);
+ end
+
+ result = self:call_agents(dialplan_object, caller, destination);
+ self.log:info('ACD ', self.id, ' PROCESS - result: ', result.disposition, ', code: ', result.code, ', wait_time: ', (os.time() - self.start_time));
+
+ if result.disposition == 'SUCCESS' then
+ acd_status = 'success';
+ break;
+ elseif os.time() < (self.start_time + self.timeout) then
+ acd_status = 'waiting';
+ else
+ break;
+ end
+ elseif acd_status == 'announce_position' then
+ acd_status = 'waiting';
+ if tostring(self.announce_position) == '0' then
+ caller.session:sayPhrase('acd_announce_position_change', caller:to_i('acd_position'));
+ else
+ caller.session:sayPhrase('acd_announce_position_periodic', caller:to_i('acd_position'));
+ end
+ end
+
+ retry_timeout = tonumber(self.record.retry_timeout);
+ end
+
+ if self.goodbye and caller.session:ready() then
+ caller.session:sayPhrase('acd_goodbye', self.goodbye);
+ end
+ self.log:info('ACD ', self.id, ' EXIT - status: ', acd_status, ', wait_time: ', (os.time() - self.start_time));
+
+ return result;
+end
diff --git a/misc/freeswitch/scripts/dialplan/callthrough.lua b/misc/freeswitch/scripts/dialplan/callthrough.lua
new file mode 100644
index 0000000..69a0611
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/callthrough.lua
@@ -0,0 +1,148 @@
+-- CommonModule: Callthrough
+--
+module(...,package.seeall)
+
+Callthrough = {}
+
+-- Create Callthrough object
+function Callthrough.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self)
+ self.__index = self
+ self.log = arg.log
+ self.database = arg.database
+ self.record = arg.record
+ self.access_authorizations = arg.access_authorizations
+ return object
+end
+
+-- Find Callthrough by ID
+function Callthrough.find_by_id(self, id)
+ local sql_query = string.format("SELECT * FROM `callthroughs` WHERE `id`=%d LIMIT 1", id)
+ local record = nil
+
+ self.database:query(sql_query, function(callthrough_entry)
+ record = callthrough_entry
+ end)
+
+ if record then
+ local callthrough = Callthrough:new(self);
+ callthrough.record = record
+
+ require 'dialplan.access_authorizations'
+ callthrough.access_authorizations = dialplan.access_authorizations.AccessAuthorization:new{ log = self.log, database = self.database }:list_by_owner(record.id, 'Callthrough');
+ return callthrough
+ end
+
+ return nil
+end
+
+function Callthrough.authenticate(self, caller)
+ local authorizations = {}
+ local logins = {}
+ local pins = {}
+
+ caller:answer();
+ caller:sleep(1000);
+
+ if not self.access_authorizations or table.getn(self.access_authorizations) == 0 then
+ self.log:debug('CALLTHROUGH_AUTHENTICATE - authorization disabled');
+ return true;
+ end
+
+ self.log:debug('CALLTHROUGH_AUTHENTICATE - access_authorizations: ', #self.access_authorizations);
+ for index, authorization in ipairs(self.access_authorizations) do
+ if authorization.phone_number then
+ if authorization.phone_number == caller.caller_phone_number then
+ if authorization.pin and authorization.pin ~= "" then
+ if caller.session:read(authorization.pin:len(), authorization.pin:len(), "ivr/ivr-please_enter_pin_followed_by_pound.wav", 3000, "#") ~= authorization.pin then
+ self.log:debug("CALLTHROUGH_AUTHENTICATE - Wrong PIN");
+ return false;
+ else
+ self.log:debug("CALLTHROUGH_AUTHENTICATE - Caller was authenticated by caller id: " .. caller.caller_phone_number .. " and PIN");
+ return authorization;
+ end
+ end
+ self.log:debug("CALLTHROUGH_AUTHENTICATE - Caller was authenticated by caller id: " .. caller.caller_phone_number);
+ return authorization;
+ end
+ else
+ self.log:debug('CALLTHROUGH_AUTHENTICATE - access_authorization=', authorization.id);
+ if authorization.id then
+ authorizations[authorization.id] = authorization;
+ if authorization.login and authorization.login ~= "" then
+ logins[authorization.login] = authorization;
+ elseif authorization.pin and authorization.pin ~= "" then
+ pins[authorization.pin] = authorization;
+ end
+ end
+ end
+ end
+
+ local login = nil;
+ local pin = nil;
+
+
+ if next(logins) ~= nil then
+
+ caller.session:streamFile('ivr/ivr-please_enter_the.wav');
+ caller.session:streamFile('ivr/ivr-id_number.wav');
+ login = caller.session:read(2, 10, 'ivr/ivr-followed_by_pound.wav', 3000, '#');
+ end
+
+ if login and logins[tostring(login)] then
+ if not logins[tostring(login)].pin or logins[tostring(login)].pin == '' then
+ self.log:debug("CALLTHROUGH_AUTHENTICATE - Caller was authenticated by login: " .. login .. " without PIN");
+ return logins[tostring(login)];
+ end
+ pin = caller.session:read(2, 10, "ivr/ivr-please_enter_pin_followed_by_pound.wav", 3000, "#");
+ if logins[tostring(login)].pin == pin then
+ self.log:debug("CALLTHROUGH_AUTHENTICATE - Caller was authenticated by login: " .. login .. " and PIN");
+ return logins[tostring(login)];
+ else
+ self.log:debug("CALLTHROUGH_AUTHENTICATE - Wrong PIN");
+ return false
+ end
+ end
+
+ if next(pins) ~= nil then
+ pin = caller.session:read(2, 10, "ivr/ivr-please_enter_pin_followed_by_pound.wav", 3000, "#");
+ end
+
+ self.log:debug("CALLTHROUGH_AUTHENTICATE - No such login, try PIN");
+
+ if pin and pins[tostring(pin)] then
+ self.log:debug("CALLTHROUGH_AUTHENTICATE - Caller was authenticated by PIN");
+ return pins[tostring(pin)];
+ end
+
+ self.log:debug("CALLTHROUGH_AUTHENTICATE - No login, wrong PIN - giving up");
+
+ return false;
+end
+
+function Callthrough.whitelist(self, number)
+ local sql_query = 'SELECT `id` FROM `whitelists` WHERE `whitelistable_type` = "Callthrough" AND `whitelistable_id` = ' .. self.record.id;
+ local whitelist_ids = {}
+
+ self.database:query(sql_query, function(entry)
+ table.insert(whitelist_ids, entry.id);
+ end)
+
+ if next(whitelist_ids) == nil then
+ return true;
+ end
+
+ -- OPTIMIZE Make sure number contains only valid characters
+ local sql_query = 'SELECT `id` FROM `phone_numbers` WHERE \
+ `number` = "' .. number .. '" AND \
+ `phone_numberable_type` = "Whitelist" AND `phone_numberable_id` IN (' .. table.concat(whitelist_ids, ',') .. ') LIMIT 1';
+
+ local authorized = false
+ self.database:query(sql_query, function(entry)
+ authorized = true
+ end)
+
+ return authorized;
+end
diff --git a/misc/freeswitch/scripts/dialplan/cdr.lua b/misc/freeswitch/scripts/dialplan/cdr.lua
new file mode 100644
index 0000000..55a7889
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/cdr.lua
@@ -0,0 +1,71 @@
+-- Gemeinschaft 5 module: cdr class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+Cdr = {}
+
+local DEFAULT_MEMBER_TIMEOUT = 20;
+
+-- Create Cdr object
+function Cdr.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.log = arg.log;
+ self.database = arg.database;
+ return object;
+end
+
+
+function Cdr.save(self, caller, destination)
+ require 'common.str'
+ local cdr = {}
+ cdr.uuid = common.str.to_sql(caller.uuid);
+ cdr.bleg_uuid = common.str.to_sql(caller:to_s('bridge_uuid'));
+ cdr.dialed_number = common.str.to_sql(caller.called_number);
+ cdr.destination_number = common.str.to_sql(destination.number);
+ cdr.caller_id_number = common.str.to_sql(caller:to_s('effective_caller_id_number'));
+ cdr.caller_id_name = common.str.to_sql(caller:to_s('effective_caller_id_name'));
+ cdr.callee_id_number = common.str.to_sql(caller:to_s('effective_callee_id_number'));
+ cdr.callee_id_name = common.str.to_sql(caller:to_s('effective_callee_id_name'));
+ cdr.start_stamp = 'FROM_UNIXTIME(' .. math.floor(caller:to_i('created_time') / 1000000) .. ')';
+ cdr.answer_stamp = 'FROM_UNIXTIME(' .. math.floor(caller:to_i('answered_time') / 1000000) .. ')';
+ cdr.end_stamp = 'NOW()';
+ cdr.duration = 'UNIX_TIMESTAMP(NOW()) - ' .. math.floor(caller:to_i('created_time') / 1000000);
+ cdr.hangup_cause = common.str.to_sql(caller.session:hangupCause());
+ cdr.dialstatus = common.str.to_sql(caller:to_s('DIALSTATUS'));
+ cdr.forwarding_number = common.str.to_sql(caller.forwarding_number);
+ cdr.forwarding_service = common.str.to_sql(caller.forwarding_service);
+
+ if caller.auth_account then
+ cdr.forwarding_account_id = common.str.to_sql(caller.auth_account.id);
+ cdr.forwarding_account_type = common.str.to_sql(caller.auth_account.class);
+ end
+
+ if caller.account then
+ cdr.account_id = common.str.to_sql(caller.account.id);
+ cdr.account_type = common.str.to_sql(caller.account.class);
+ end
+
+ if caller:to_i('answered_time') > 0 then
+ cdr.billsec = 'UNIX_TIMESTAMP(NOW()) - ' .. math.floor(caller:to_i('answered_time') / 1000000);
+ end
+
+ cdr.bleg_account_id = common.str.to_sql(tonumber(destination.id));
+ cdr.bleg_account_type = common.str.to_sql(destination.type);
+
+ local keys = {}
+ local values = {}
+
+ for key, value in pairs(cdr) do
+ table.insert(keys, key);
+ table.insert(values, value);
+ end
+
+ local sql_query = 'INSERT INTO `cdrs` (`' .. table.concat(keys, "`, `") .. '`) VALUES (' .. table.concat(values, ", ") .. ')';
+ self.log:info('CDR_SAVE - caller: ', cdr.account_type, '=', cdr.account_id, ', callee: ',cdr.bleg_account_type, '=', cdr.bleg_account_id,', dialstatus: ', cdr.dialstatus);
+ return self.database:query(sql_query);
+end
diff --git a/misc/freeswitch/scripts/dialplan/dialplan.lua b/misc/freeswitch/scripts/dialplan/dialplan.lua
new file mode 100644
index 0000000..f4dca9e
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/dialplan.lua
@@ -0,0 +1,996 @@
+-- Gemeinschaft 5 module: dialplan class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+Dialplan = {}
+
+-- local constants
+local CONFIG_FILE_NAME = '/opt/freeswitch/scripts/ini/dialplan.ini';
+local DIAL_TIMEOUT = 120;
+local MAX_LOOPS = 20;
+local DIALPLAN_FUNCTION_PATTERN = '^f[_%-].*';
+local CALL_FORWARDING_SERVICES = {
+ USER_BUSY = 'busy',
+ CALL_REJECTED = 'busy',
+ NO_ANSWER = 'noanswer',
+ USER_NOT_REGISTERED = 'offline',
+ HUNT_GROUP_EMPTY = 'offline',
+ ACD_NO_AGENTS = 'offline',
+ ACD_TIMEOUT = 'noanswer',
+}
+
+-- create dialplan object
+function Dialplan.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.log = arg.log;
+ self.database = arg.database;
+ self.caller = arg.caller;
+
+ return object;
+end
+
+
+function Dialplan.domain_get(self, domain)
+ require 'common.str'
+ local global_domain = freeswitch.API():execute('global_getvar', 'domain');
+
+ if common.str.blank(global_domain) then
+ if common.str.blank(domain) then
+ require 'common.database'
+ local database = common.database.Database:new{ log = self.log }:connect();
+ if not database:connected() then
+ self.log:error('[', uuid,'] DIALPLAN_DOMAIN - cannot connect to Gemeinschaft database');
+ else
+ require 'configuration.sip'
+ local domains = configuration.sip.Sip:new{ log = self.log, database = database }:domains();
+ if domains[1] then
+ domain = domains[1]['host'];
+ end
+ end
+ end
+
+ if database then
+ database:release();
+ end
+
+ if not common.str.blank(domain) then
+ self.log:notice('DIALPLAN_DOMAIN - setting default domain: ', domain);
+ freeswitch.API():execute('global_setvar', 'domain=' .. tostring(domain));
+ end
+ else
+ domain = global_domain;
+ end
+
+ if common.str.blank(domain) then
+ self.log:error('DIALPLAN_DOMAIN - no domain found');
+ end
+
+ return domain;
+end
+
+
+function Dialplan.configuration_read(self, file_name)
+ require 'common.str'
+ require 'common.configuration_file'
+
+ -- dialplan configuration
+ self.config = common.configuration_file.get(file_name or CONFIG_FILE_NAME);
+ self.node_id = common.str.to_i(self.config.parameters.node_id);
+ self.domain = self:domain_get(self.config.parameters.domain);
+ self.dial_timeout = tonumber(self.config.parameters.dial_timeout) or DIAL_TIMEOUT;
+ self.max_loops = tonumber(self.config.parameters.max_loops) or MAX_LOOPS;
+ self.user_image_url = common.str.to_s(self.config.parameters.user_image_url);
+ self.phone_book_entry_image_url = common.str.to_s(self.config.parameters.phone_book_entry_image_url);
+ self.phonebook_number_lookup = common.str.to_b(self.config.parameters.phonebook_number_lookup);
+ self.geo_number_lookup = common.str.to_b(self.config.parameters.geo_number_lookup);
+ self.default_language = self.config.parameters.default_language or 'en';
+ self.send_ringing_to_gateways = common.str.to_b(self.config.parameters.send_ringing_to_gateways);
+
+ if tonumber(self.config.parameters.default_ringtone) then
+ self.default_ringtone = 'http://amooma.de;info=Ringer' .. self.config.parameters.default_ringtone .. ';x-line-id=0';
+ else
+ self.default_ringtone = 'http://amooma.de;info=Ringer1;x-line-id=0';
+ end
+
+ return (self.config ~= nil);
+end
+
+
+function Dialplan.hangup(self, code, phrase, cause)
+ if self.caller:ready() then
+ if tonumber(code) then
+ self.caller:respond(code, phrase or 'Thank you for flying Gemeinschaft5');
+ end
+ self.caller:hangup(cause or 16);
+ else
+ self.log:info('HANGUP - caller sesson down - cause: ', self.caller.session:hangupCause());
+ end
+end
+
+
+function Dialplan.check_auth(self)
+ local authenticated = false;
+
+ require 'common.str'
+ if self.caller.from_node then
+ self.log:info('AUTH_FIRST_STAGE - node authenticated - node_id: ', self.caller.node_id);
+ authenticated = true;
+ elseif not common.str.blank(self.caller.auth_account_type) then
+ self.log:info('AUTH_FIRST_STAGE - sipaccount autheticated by name/password: ', self.caller.auth_account_type, '=', self.caller.account_id, '/', self.caller.account_uuid);
+ authenticated = true;
+ elseif self.caller.from_gateway then
+ self.log:info('AUTH_FIRST_STAGE - gateway autheticated by name/password: gateway=', self.caller.gateway_id, ', name: ', self.caller.gateway_name);
+ authenticated = true;
+ else
+ local gateways = common.configuration_file.get('/opt/freeswitch/scripts/ini/gateways.ini', false);
+ if not gateways then
+ return false;
+ end
+ for gateway, gateway_parameters in pairs(gateways) do
+ if common.str.to_s(gateway_parameters.proxy) == self.caller.sip_contact_host then
+ self.caller.gateway_name = gateway;
+ self.caller.from_gateway = true;
+ self.log:info('AUTH_FIRST_STAGE - gateway autheticated by ip: gateway=', self.caller.gateway_id, ', name: ', self.caller.gateway_name, ', ip: ', self.caller.sip_contact_host);
+ authenticated = true;
+ end
+ end
+ end
+
+ return authenticated;
+end
+
+
+function Dialplan.check_auth_node(self)
+ require 'common.node'
+ local node = common.node.Node:new{ log = self.log, database = self.database }:find_by_address(self.caller.sip_contact_host);
+
+ return (node ~= nil);
+end
+
+
+function Dialplan.check_auth_ip(self)
+ self.log:info('AUTH - node: ', self.caller.from_node, ', auth_account: ', self.caller.auth_account_type, ', gateway: ', self.caller.from_gateway);
+ require 'common.str'
+ if self.caller.from_node then
+ return true;
+ elseif not common.str.blank(self.caller.auth_account_type) then
+ return true;
+ elseif self.caller.from_gateway then
+ return true;
+ else
+ return nil;
+ end
+end
+
+
+function Dialplan.object_find(self, class, identifier, auth_name)
+ require 'common.str'
+ class = common.str.downcase(class);
+
+ if class == 'user' then
+ require 'dialplan.user'
+ local user = nil;
+ if type(identifier) == 'number' then
+ user = dialplan.user.User:new{ log = self.log, database = self.database }:find_by_id(identifier);
+ else
+ user = dialplan.user.User:new{ log = self.log, database = self.database }:find_by_uuid(identifier);
+ end
+
+ if user then
+ user.groups = user:list_groups();
+ end
+
+ return user;
+ elseif class == 'tenant' then
+ require 'dialplan.tenant'
+ local tenant = nil;
+ if type(identifier) == 'number' then
+ tenant = dialplan.tenant.Tenant:new{ log = self.log, database = self.database }:find_by_id(identifier);
+ else
+ tenant = dialplan.tenant.Tenant:new{ log = self.log, database = self.database }:find_by_uuid(identifier);
+ end
+
+ return tenant;
+ elseif class == 'sipaccount' then
+ require 'common.sip_account'
+ local sip_account = nil;
+ if auth_name then
+ sip_account = common.sip_account.SipAccount:new{ log = self.log, database = self.database }:find_by_auth_name(auth_name, identifier);
+ elseif type(identifier) == 'number' then
+ sip_account = common.sip_account.SipAccount:new{ log = self.log, database = self.database }:find_by_id(identifier);
+ else
+ sip_account = common.sip_account.SipAccount:new{ log = self.log, database = self.database }:find_by_uuid(identifier);
+ end
+ if sip_account then
+ sip_account.owner = self:object_find(sip_account.record.sip_accountable_type, tonumber(sip_account.record.sip_accountable_id));
+ end
+ return sip_account;
+ elseif class == 'huntgroup' then
+ require 'dialplan.hunt_group'
+
+ local hunt_group = nil;
+ if type(identifier) == 'number' then
+ hunt_group = dialplan.hunt_group.HuntGroup:new{ log = self.log, database = self.database }:find_by_id(identifier);
+ else
+ hunt_group = dialplan.hunt_group.HuntGroup:new{ log = self.log, database = self.database }:find_by_uuid(identifier);
+ end
+
+ if hunt_group then
+ hunt_group.owner = self:object_find('tenant', tonumber(hunt_group.record.tenant_id));
+ end
+
+ return hunt_group;
+ elseif class == 'automaticcalldistributor' then
+ require 'dialplan.acd'
+
+ local acd = nil;
+ if type(identifier) == 'number' then
+ acd = dialplan.acd.AutomaticCallDistributor:new{ log = self.log, database = self.database, domain = self.domain }:find_by_id(identifier);
+ else
+ acd = dialplan.acd.AutomaticCallDistributor:new{ log = self.log, database = self.database, domain = self.domain }:find_by_uuid(identifier);
+ end
+
+ if acd then
+ acd.owner = self:object_find(acd.record.automatic_call_distributorable_type, tonumber(acd.record.automatic_call_distributorable_id));
+ end
+
+ return acd;
+ elseif class == 'faxaccount' then
+ require 'dialplan.fax'
+ local fax_account = nil;
+ if type(identifier) == 'number' then
+ fax_account = dialplan.fax.Fax:new{ log = self.log, database = self.database }:find_by_id(identifier);
+ else
+ fax_account = dialplan.fax.Fax:new{ log = self.log, database = self.database }:find_by_uuid(identifier);
+ end
+ if fax_account then
+ fax_account.owner = self:object_find(fax_account.record.fax_accountable_type, tonumber(fax_account.record.fax_accountable_id));
+ end
+
+ return fax_account;
+ end
+end
+
+
+function Dialplan.retrieve_caller_data(self)
+ self.caller.caller_phone_numbers_hash = {}
+
+ require 'common.str'
+
+ local dialed_sip_user = self.caller:to_s('dialed_user');
+
+ -- TODO: Set auth_account on transfer initiated by calling party
+ if not common.str.blank(dialed_sip_user) then
+ self.caller.auth_account = self:object_find('sipaccount', self.caller:to_s('dialed_domain'), dialed_sip_user);
+ self.caller:set_auth_account(self.caller.auth_account);
+ elseif not common.str.blank(self.caller.auth_account_type) and not common.str.blank(self.caller.auth_account_uuid) then
+ self.caller.auth_account = self:object_find(self.caller.auth_account_type, self.caller.auth_account_uuid);
+ self.caller:set_auth_account(self.caller.auth_account);
+ end
+
+ if self.caller.auth_account then
+ self.log:info('CALLER_DATA - auth account: ', self.caller.auth_account.class, '=', self.caller.auth_account.id, '/', self.caller.auth_account.uuid);
+ if self.caller.auth_account.owner then
+ self.log:info('CALLER_DATA - auth owner: ', self.caller.auth_account.owner.class, '=', self.caller.auth_account.owner.id, '/', self.caller.auth_account.owner.uuid);
+ else
+ self.log:error('CALLER_DATA - auth owner not found');
+ end
+ else
+ self.log:info('CALLER_DATA - no data - unauthenticated call: ', self.caller.auth_account_type, '/', self.caller.auth_account_uuid);
+ end
+
+ if not common.str.blank(self.caller.account_type) and not common.str.blank(self.caller.account_uuid) then
+ self.caller.account = self:object_find(self.caller.account_type, self.caller.account_uuid);
+ if self.caller.account then
+ require 'common.phone_number'
+ self.caller.caller_phone_numbers = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database }:list_by_owner(self.caller.account.id, self.caller.account.class);
+ for index, caller_number in ipairs(self.caller.caller_phone_numbers) do
+ self.caller.caller_phone_numbers_hash[caller_number] = true;
+ end
+ self.log:info('CALLER_DATA - caller account: ', self.caller.account.class, '=', self.caller.account.id, '/', self.caller.account.uuid, ', phone_numbers: ', #self.caller.caller_phone_numbers);
+ if self.caller.account.owner then
+ self.log:info('CALLER_DATA - caller owner: ', self.caller.account.owner.class, '=', self.caller.account.owner.id, '/', self.caller.account.owner.uuid);
+ else
+ self.log:error('CALLER_DATA - caller owner not found');
+ end
+
+ if not self.caller.clir then
+ self.caller:set_caller_id(self.caller.caller_phone_numbers[1], self.caller.account.record.caller_name or self.caller.account.record.name);
+ end
+ else
+ self.log:error('CALLER_DATA - caller account not found: ', self.caller.account_type, '/', self.caller.account_uuid);
+ end
+ end
+end
+
+
+function Dialplan.destination_new(self, arg)
+ require 'common.str'
+
+ local destination = {
+ number = arg.number or '',
+ type = arg.type or 'unknown',
+ id = common.str.to_i(arg.id),
+ uuid = arg.uuid or '',
+ phone_number = arg.phone_number,
+ node_id = common.str.to_i(arg.node_id),
+ call_forwarding = {},
+ data = arg.data,
+ }
+
+ destination.type = common.str.downcase(destination.type);
+
+ if not common.str.blank(destination.number) then
+ if destination.type == 'unknown' and destination.number:find(DIALPLAN_FUNCTION_PATTERN) then
+ destination.type = 'dialplanfunction';
+ elseif destination.type == 'phonenumber' or destination.type == 'unknown' then
+ require 'common.phone_number'
+ destination.phone_number = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database }:find_by_number(destination.number);
+
+ if destination.phone_number then
+ destination.type = common.str.downcase(destination.phone_number.record.phone_numberable_type);
+ destination.id = common.str.to_i(destination.phone_number.record.phone_numberable_id);
+ destination.uuid = common.str.to_s(destination.phone_number.record.phone_numberable_uuid);
+ destination.node_id = common.str.to_i(destination.phone_number.record.gs_node_id);
+ if self.caller then
+ destination.call_forwarding = destination.phone_number:call_forwarding(self.caller.caller_phone_numbers);
+ end
+ elseif destination.type == 'unknown' then
+ require 'common.sip_account'
+ destination.account = common.sip_account.SipAccount:new{ log = self.log, database = self.database }:find_by_auth_name(destination.number);
+ if destination.account then
+ destination.type = 'sipaccount';
+ destination.id = common.str.to_i(destination.account.record.id);
+ destination.uuid = common.str.to_s(destination.account.record.uuid);
+ destination.node_id = common.str.to_i(destination.account.record.gs_node_id);
+ end
+ end
+ end
+ end
+
+ if destination.node_id == 0 then
+ destination.node_id = self.node_id;
+ destination.node_local = true;
+ else
+ destination.node_local = (destination.node_id == self.node_id);
+ end
+
+ self.log:info('DESTINATION_NEW - ', destination.type, '=', destination.id, '/', destination.uuid,'@', destination.node_id, ', number: ', destination.number);
+
+ return destination;
+end
+
+
+function Dialplan.routes_get(self, destination)
+ require 'dialplan.route'
+ return dialplan.route.Route:new{ log = self.log, database = self.database, routing_table = self.routes }:outbound(self.caller, destination.number);
+end
+
+
+function Dialplan.set_caller_picture(self, entry_id, entry_type, image)
+ entry_type = entry_type:lower();
+ if entry_type == 'user' then
+ require 'dialplan.user'
+ local user = dialplan.user.User:new{ log = self.log, database = self.database }:find_by_id(entry_id);
+ if user then
+ self.caller:set_variable('sip_h_Call-Info', '<' .. self.user_image_url .. '/' .. tonumber(entry_id) .. '/snom_caller_picture_' .. tostring(user.record.image) .. '>;purpose=icon');
+ end
+ elseif entry_type == 'phonebookentry' and image then
+ self.caller:set_variable('sip_h_Call-Info', '<' .. self.phone_book_entry_image_url .. '/' .. tonumber(entry_id) .. '/snom_caller_picture_' .. tostring(image) .. '>;purpose=icon');
+ end
+end
+
+
+function Dialplan.dial(self, destination)
+ require 'common.str'
+ destination.caller_id_number = destination.caller_id_number or self.caller.caller_phone_numbers[1];
+
+ if not self.caller.clir then
+ if destination.node_local and destination.type == 'sipaccount' then
+ local user_id = nil;
+ local tenant_id = nil;
+
+ destination.account = self:object_find(destination.type, destination.id);
+ if destination.account then
+ if destination.account.class == 'sipaccount' then
+ destination.callee_id_name = destination.account.record.caller_name;
+ self.caller:set_callee_id(destination.number, destination.account.record.caller_name);
+ end
+ end
+
+ if destination.account and destination.account.owner then
+ if destination.account.owner.class == 'user' then
+ user_id = destination.account.owner.id;
+ tenant_id = tonumber(destination.account.owner.record.current_tenant_id);
+ elseif destination.account.owner.class == 'tenant' then
+ tenant_id = destination.account.owner.id;
+ end
+ end
+
+ if user_id or tenant_id then
+ require 'common.str'
+ local phone_book_entry = nil;
+
+ if self.phonebook_number_lookup then
+ require 'dialplan.phone_book'
+ phone_book_entry = dialplan.phone_book.PhoneBook:new{ log = self.log, database = self.database }:find_entry_by_number_user_tenant(self.caller.caller_phone_numbers, user_id, tenant_id);
+ end
+
+ if phone_book_entry then
+ self.log:info('PHONE_BOOK_ENTRY - phone_book=', phone_book_entry.phone_book_id, ' (', phone_book_entry.phone_book_name, '), caller_id_name: ', phone_book_entry.caller_id_name, ', ringtone: ', phone_book_entry.bellcore_id);
+ destination.caller_id_name = common.str.to_ascii(phone_book_entry.caller_id_name);
+ if tonumber(phone_book_entry.bellcore_id) then
+ self.log:debug('RINGTONE - phonebookentry, index: ', phone_book_entry.bellcore_id);
+ self.caller:export_variable('alert_info', 'http://amooma.de;info=Ringer' .. phone_book_entry.bellcore_id .. ';x-line-id=0');
+ end
+ if phone_book_entry.image then
+ self:set_caller_picture(phone_book_entry.id, 'phonebookentry', phone_book_entry.image);
+ elseif self.caller.account and self.caller.account.owner then
+ self:set_caller_picture(self.caller.account.owner.id, self.caller.account.owner.class);
+ end
+ elseif self.caller.account and self.caller.account.owner then
+ self:set_caller_picture(self.caller.account.owner.id, self.caller.account.owner.class);
+ elseif self.geo_number_lookup then
+ require 'dialplan.geo_number'
+ local geo_number = dialplan.geo_number.GeoNumber:new{ log = self.log, database = self.database }:find(destination.caller_id_number);
+ if geo_number then
+ self.log:info('GEO_NUMBER - found: ', geo_number.name, ', ', geo_number.country);
+ if geo_number.name then
+ destination.caller_id_name = common.str.to_ascii(geo_number.name) .. ', ' .. common.str.to_ascii(geo_number.country);
+ else
+ destination.caller_id_name = common.str.to_ascii(geo_number.country);
+ end
+ end
+ end
+ end
+ end
+ self.caller:set_caller_id(destination.caller_id_number, destination.caller_id_name or self.caller.caller_id_name);
+ else
+ self.caller:set_caller_id('anonymous', 'Unknown');
+ self.caller:set_privacy(true);
+ end
+
+ local destinations = { destination };
+
+ if self.caller.forwarding_service == 'assistant' and self.caller.auth_account and self.caller.auth_account.class == 'sipaccount' then
+ self.caller.auth_account.type = self.caller.auth_account.class;
+ local forwarding_destination = self:destination_new( self.caller.auth_account );
+ if forwarding_destination then
+ forwarding_destination.alert_info = 'http://amooma.com;info=Ringer0;x-line-id=0'
+ table.insert(destinations, forwarding_destination);
+ end
+ end
+
+ if common.str.to_b(self.config.parameters.bypass_media) then
+ self.caller:set_variable('bypass_media', true);
+ end
+
+ require 'dialplan.sip_call'
+ return dialplan.sip_call.SipCall:new{ log = self.log, database = self.database, caller = self.caller }:fork(
+ destinations,
+ { timeout = self.dial_timeout_active,
+ send_ringing = ( self.send_ringing_to_gateways and self.caller.from_gateway ),
+ bypass_media_network = self.config.parameters.bypass_media_network,
+ }
+ );
+end
+
+
+function Dialplan.huntgroup(self, destination)
+ local hunt_group = self:object_find('huntgroup', tonumber(destination.id));
+
+ if not hunt_group then
+ self.log:error('DIALPLAN_HUNTGROUP - huntgroup not found');
+ return { continue = true, code = 404, phrase = 'Huntgroup not found' }
+ end
+
+ self.caller:set_callee_id(destination.number, hunt_group.record.name);
+ destination.caller_id_number = destination.caller_id_number or self.caller.caller_phone_numbers[1];
+
+ if not self.caller.clir then
+ self.caller:set_caller_id(destination.caller_id_number, tostring(hunt_group.record.name) .. ' '.. tostring(self.caller.caller_id_name));
+ if self.caller.account and self.caller.account.owner then
+ self:set_caller_picture(self.caller.account.owner.id, self.caller.account.owner.class);
+ end
+ else
+ self.caller:set_caller_id('anonymous', tostring(hunt_group.record.name));
+ self.caller:set_privacy(true);
+ end
+
+ self.caller.auth_account = hunt_group;
+ self.caller:set_auth_account(self.caller.auth_account);
+ self.caller.forwarding_number = destination.number;
+ self.caller.forwarding_service = 'huntgroup';
+ self.caller:set_variable('gs_forwarding_service', self.caller.forwarding_service);
+ self.caller:set_variable('gs_forwarding_number', self.caller.forwarding_number);
+ return hunt_group:run(self, self.caller, destination);
+end
+
+
+function Dialplan.acd(self, destination)
+ local acd = self:object_find('automaticcalldistributor', tonumber(destination.id));
+
+ if not acd then
+ self.log:error('DIALPLAN_ACD - acd not found');
+ return { continue = true, code = 404, phrase = 'ACD not found' }
+ end
+
+ self.caller:set_callee_id(destination.number, acd.record.name);
+ destination.caller_id_number = destination.caller_id_number or self.caller.caller_phone_numbers[1];
+
+ if not self.caller.clir then
+ self.caller:set_caller_id(destination.caller_id_number, tostring(acd.record.name) .. ' '.. tostring(self.caller.caller_id_name));
+ if self.caller.account and self.caller.account.owner then
+ self:set_caller_picture(self.caller.account.owner.id, self.caller.account.owner.class);
+ end
+ else
+ self.caller:set_caller_id('anonymous', tostring(acd.record.name));
+ self.caller:set_privacy(true);
+ end
+
+ self.caller.auth_account = acd;
+ self.caller:set_auth_account(self.caller.auth_account);
+ self.caller.forwarding_number = destination.number;
+ self.caller.forwarding_service = 'automaticcalldistributor';
+ self.caller:set_variable('gs_forwarding_service', self.caller.forwarding_service);
+ self.caller:set_variable('gs_forwarding_number', self.caller.forwarding_number);
+
+ acd:caller_new(self.caller.uuid);
+ local result = acd:run(self, self.caller, destination);
+ acd:caller_delete();
+
+ return result;
+end
+
+
+function Dialplan.conference(self, destination)
+ -- call local conference
+ require 'common.conference'
+ conference = common.conference.Conference:new{ log = self.log, database = self.database }:find_by_id(destination.id);
+
+ if not conference then
+ return { continue = false, code = 404, phrase = 'Conference not found' }
+ end
+
+ local cause = conference:enter(self.caller, self.domain);
+ return { continue = false, cause = cause }
+end
+
+
+function Dialplan.faxaccount(self, destination)
+ require 'dialplan.fax'
+ local fax_account = dialplan.fax.Fax:new{ log = self.log, database = self.database }:find_by_id(destination.id);
+
+ if not fax_account then
+ return { continue = false, code = 404, phrase = 'Fax not found' }
+ end
+
+ self.log:info('FAX_RECEIVE start - fax_account=', fax_account.id, '/', fax_account.uuid, ', name: ', fax_account.record.name, ', station_id: ', fax_account.record.station_id);
+
+ self.caller:set_caller_id(self.caller.caller_phone_number);
+ self.caller:set_callee_id(destination.number, fax_account.record.name);
+
+ local fax_document = fax_account:receive(self.caller);
+
+ if not fax_document then
+ self.log:error('FAX_RECEIVE - error receiving fax document - fax_account=', fax_account.id, '/', fax_account.uuid);
+ return { continue = false, code = 500, phrase = 'Error receiving fax' };
+ end
+
+ fax_document.caller_id_number = self.caller.caller_phone_number;
+ fax_document.caller_id_name = self.caller.caller_id_name;
+ fax_document.uuid = self.caller.uuid;
+
+ self.log:info('FAX_RECEIVE end - success: ', fax_document.success,
+ ', remote: ', fax_document.remote_station_id,
+ ', pages: ', fax_document.transferred_pages, '/', fax_document.total_pages,
+ ', result: ', fax_document.result_code, ' ', fax_document.result_text);
+
+ if fax_document.success then
+ self.log:notice('FAX_RECEIVE - saving fax document: ', fax_document.filename );
+ if not fax_account:insert_document(fax_document) then
+ self.log:error('FAX_RECEIVE - error inserting fax document to database - fax_account=', fax_account.id, '/', fax_account.uuid, ', file: ', fax_document.filename);
+ end
+ end
+
+ return { continue = false, code = 200, phrase = 'OK' }
+end
+
+
+function Dialplan.callthrough(self, destination)
+ -- Callthrough
+ require 'dialplan.callthrough'
+ callthrough = dialplan.callthrough.Callthrough:new{ log = self.log, database = self.database }:find_by_id(destination.id)
+
+ if not callthrough then
+ self.log:error('CALLTHROUGH - no callthrough for destination number: ', destination.number);
+ return { continue = false, code = 404, phrase = 'Fax not found' }
+ end
+ self.log:info('CALLTHROUGH - number: ' .. destination.number .. ', name: ' .. callthrough.record.name);
+
+ local authorization = callthrough:authenticate(self.caller);
+
+ if not authorization then
+ self.log:notice('CALLTHROUGH - authentication failed');
+ return { continue = false, code = 403, phrase = 'Authentication failed' }
+ end
+
+ if type(authorization) == 'table' and tonumber(authorization.sip_account_id) and tonumber(authorization.sip_account_id) > 0 then
+ local auth_account = self:object_find('sipaccount', tonumber(authorization.sip_account_id));
+ self.caller.forwarding_number = destination.number;
+ self.caller.forwarding_service = 'callthrough';
+ self.caller:set_variable('gs_forwarding_service', self.caller.forwarding_service);
+ self.caller:set_variable('gs_forwarding_number', self.caller.forwarding_number);
+
+ if auth_account then
+ self.caller.auth_account = auth_account;
+ self.caller:set_auth_account(self.caller.auth_account);
+ self.log:info('AUTH_ACCOUNT_UPDATE - account: ', self.caller.auth_account.class, '=', self.caller.auth_account.id, '/', self.caller.auth_account.uuid);
+ if self.caller.auth_account.owner then
+ self.log:info('AUTH_ACCOUNT_UPDATE - auth owner: ', self.caller.auth_account.owner.class, '=', self.caller.auth_account.owner.id, '/', self.caller.auth_account.owner.uuid);
+ else
+ self.log:error('AUTH_ACCOUNT_UPDATE - auth owner not found');
+ end
+ self.log:info('CALLTHROUGH - use sip account: ', auth_account.id, ' (', auth_account.record.caller_name, ')');
+ end
+ else
+ self.log:info('CALLTHROUGH - no sip account');
+ end
+
+ local destination_number = '';
+ for i = 1, 3, 1 do
+ if destination_number ~= '' then
+ break;
+ end
+ destination_number = session:read(2, 16, "ivr/ivr-enter_destination_telephone_number.wav", 3000, "#");
+ end
+
+ if destination_number == '' then
+ self.log:debug("no callthrough destination - hangup call");
+ return { continue = false, code = 404, phrase = 'No destination' }
+ end
+
+ local route = dialplan.route.Route:new{ log = self.log, database = self.database, routing_table = self.routes }:prerouting(self.caller, destination_number);
+ if route and route.value then
+ destination_number = route.value;
+ end
+
+ if not callthrough:whitelist(destination_number) then
+ self.log:debug('caller not authorized to call destination number: ' .. destination_number .. ' - hangup call');
+ return { continue = false, code = 403, phrase = 'Unauthorized' }
+ end
+
+ return { continue = true, code = 302, number = destination_number }
+end
+
+
+function Dialplan.voicemail(self, destination)
+ if not self.caller.auth_account or self.caller.auth_account.class ~= 'sipaccount' then
+ self.log:error('VOICEMAIL - incompatible destination');
+ return { continue = false, code = 404, phrase = 'Mailbox not found' }
+ end
+
+ require 'dialplan.voicemail'
+ local voicemail_account = dialplan.voicemail.Voicemail:new{ log = self.log, database = self.database }:find_by_sip_account_id(self.caller.auth_account.id);
+
+ if not voicemail_account then
+ self.log:error('VOICEMAIL - no mailbox');
+ return { continue = false, code = 404, phrase = 'Mailbox not found' }
+ end
+
+ voicemail_account:leave(self.caller, self.caller.forwarding_number);
+
+ if self.caller:to_s("voicemail_message_len") == '' then
+ self.log:info('VOICEMAIL - no message saved');
+ end
+
+ return { continue = false, code = 200 }
+end
+
+
+function Dialplan.dialplanfunction(self, destination)
+ require 'dialplan.functions'
+ return dialplan.functions.Functions:new{ log = self.log, database = self.database, domain = self.domain }:dialplan_function(self.caller, destination.number);
+end
+
+
+function Dialplan.switch(self, destination)
+ require 'common.str'
+ local result = nil;
+ self.dial_timeout_active = self.dial_timeout;
+
+ if not destination.node_local then
+ return self:dial(destination);
+ end
+
+ for service, call_forwarding in pairs(destination.call_forwarding) do
+ if self.caller.caller_phone_numbers_hash[call_forwarding.number] then
+ self.log:info('CALL_FORWARDING - caller number equals destination: ', call_forwarding.number,' - ignore service: ', service);
+ destination.call_forwarding[service] = nil;
+ end
+ end
+
+ if destination.call_forwarding.noanswer then
+ self.dial_timeout_active = tonumber(destination.call_forwarding.noanswer.timeout) or self.dial_timeout;
+ end
+
+ if destination.call_forwarding.always then
+ return { continue = true, call_forwarding = destination.call_forwarding.always }
+ elseif destination.call_forwarding.assistant then
+ if common.str.downcase(destination.call_forwarding.assistant.type) == 'huntgroup' then
+ require 'dialplan.hunt_group'
+ local hunt_group = dialplan.hunt_group.HuntGroup:new{ log = self.log, database = self.database }:find_by_id(destination.call_forwarding.assistant.id);
+ self.log:info('CALL_FORWARDING - huntgroup - auth_account: ', self.caller.auth_account_type, '=', self.caller.auth_account_uuid);
+ if hunt_group and (hunt_group:is_member_by_numbers(self.caller.caller_phone_numbers)) then
+ self.log:info('CALL_FORWARDING - caller is huntgroup member - ignore service: ', destination.call_forwarding.assistant.service);
+ else
+ return { continue = true, call_forwarding = destination.call_forwarding.assistant }
+ end
+ else
+ return { continue = true, call_forwarding = destination.call_forwarding.assistant }
+ end
+ end
+
+ -- reset ringtone
+ self.caller:export_variable('alert_info', self.default_ringtone);
+
+ if destination.phone_number then
+ local ringtone = destination.phone_number:ringtone();
+ if ringtone and ringtone.bellcore_id then
+ self.log:debug('RINGTONE - ', ringtone.ringtoneable_type .. ', index: ' .. ringtone.bellcore_id);
+ self.caller:export_variable('alert_info', 'http://amooma.de;info=Ringer' .. tonumber(ringtone.bellcore_id) .. ';x-line-id=0');
+ end
+ end
+
+ if destination.type == 'sipaccount' then
+ result = self:dial(destination);
+ if CALL_FORWARDING_SERVICES[result.disposition] then
+ result.call_forwarding = destination.call_forwarding[CALL_FORWARDING_SERVICES[result.disposition]];
+ if result.call_forwarding then
+ result.continue = true;
+ end
+ end
+ return result;
+ elseif destination.type == 'conference' then
+ return self:conference(destination);
+ elseif destination.type == 'faxaccount' then
+ return self:faxaccount(destination);
+ elseif destination.type == 'callthrough' then
+ return self:callthrough(destination);
+ elseif destination.type == 'huntgroup' then
+ result = self:huntgroup(destination);
+ if CALL_FORWARDING_SERVICES[result.disposition] then
+ result.call_forwarding = destination.call_forwarding[CALL_FORWARDING_SERVICES[result.disposition]];
+ if result.call_forwarding then
+ result.continue = true;
+ end
+ end
+ return result;
+ elseif destination.type == 'automaticcalldistributor' then
+ result = self:acd(destination);
+ if CALL_FORWARDING_SERVICES[result.disposition] then
+ result.call_forwarding = destination.call_forwarding[CALL_FORWARDING_SERVICES[result.disposition]];
+ if result.call_forwarding then
+ result.continue = true;
+ end
+ end
+ return result;
+ elseif destination.type == 'voicemail' then
+ return self:voicemail(destination);
+ elseif destination.type == 'dialplanfunction' then
+ return self:dialplanfunction(destination);
+ elseif not common.str.blank(destination.number) then
+ local result = { continue = false, code = 404, phrase = 'No route' }
+ local routes = self:routes_get(destination);
+
+ if not routes or #routes == 0 then
+ self.log:notice('SWITCH - no route - number: ', destination.number);
+ return { continue = false, code = 404, phrase = 'No route' }
+ end
+
+ destination.callee_id_number = destination.number;
+ destination.callee_id_name = nil;
+
+ if self.phonebook_number_lookup then
+ require 'common.str'
+ local user_id = common.str.try(self.caller, 'account.owner.id');
+ local tenant_id = common.str.try(self.caller, 'account.owner.record.current_tenant_id');
+
+ if user_id or tenant_id then
+ require 'dialplan.phone_book'
+ local phone_book_entry = dialplan.phone_book.PhoneBook:new{ log = self.log, database = self.database }:find_entry_by_number_user_tenant({ destination.number }, user_id, tenant_id);
+ if phone_book_entry then
+ self.log:info('PHONE_BOOK_ENTRY - phone_book=', phone_book_entry.phone_book_id, ' (', phone_book_entry.phone_book_name, '), callee_id_name: ', common.str.to_ascii(phone_book_entry.caller_id_name));
+ destination.callee_id_name = common.str.to_ascii(phone_book_entry.caller_id_name);
+ end
+ end
+ end
+
+ if self.geo_number_lookup and not destination.callee_id_name then
+ require 'dialplan.geo_number'
+ local geo_number = dialplan.geo_number.GeoNumber:new{ log = self.log, database = self.database }:find(destination.number);
+ if geo_number then
+ require 'common.str'
+ self.log:info('GEO_NUMBER - found: ', geo_number.name, ', ', geo_number.country);
+ if geo_number.name then
+ destination.callee_id_name = common.str.to_ascii(geo_number.name) .. ', ' .. common.str.to_ascii(geo_number.country);
+ else
+ destination.callee_id_name = common.str.to_ascii(geo_number.country);
+ end
+ end
+ end
+
+ self.caller:set_callee_id(destination.callee_id_number, destination.callee_id_name);
+
+ for index, route in ipairs(routes) do
+ if route.class == 'hangup' then
+ return { continue = false, code = route.endpoint, phrase = route.phrase, cause = route.value }
+ end
+ if route.class == 'forward' then
+ return { continue = true, call_forwarding = { number = route.value, service = 'route', type = 'phonenumber' }}
+ end
+ destination.gateway = route.endpoint;
+ destination.type = route.class;
+ destination.number = route.value;
+ destination.caller_id_number = route.caller_id_number;
+ destination.caller_id_name = route.caller_id_name;
+ result = self:dial(destination);
+
+ if result.continue == false then
+ break;
+ end
+
+ if common.str.to_b(self.routes.failover[tostring(result.code)]) == true then
+ self.log:info('SWITCH - failover - code: ', result.code);
+ elseif common.str.to_b(self.routes.failover[tostring(result.cause)]) == true then
+ self.log:info('SWITCH - failover - cause: ', result.cause);
+ else
+ self.log:info('SWITCH - no failover - cause: ', result.cause, ', code: ', result.code);
+ break;
+ end
+ end
+
+ return result;
+ end
+
+ self.log:error('SWITCH - destination not found - type: ', destination.type);
+ return { continue = true, code = 404, phrase = destination.type .. ' not found' }
+end
+
+
+function Dialplan.run(self, destination)
+ self.caller:set_variable('hangup_after_bridge', false);
+ self.caller:set_variable('ringback', self.config.parameters.ringback);
+ self.caller:set_variable('bridge_early_media', 'true');
+ self.caller:set_variable('send_silence_when_idle', 0);
+ self.caller:set_variable('default_language', self.default_language);
+ self.caller:set_variable('gs_save_cdr', true);
+ self.caller:set_variable('gs_call_service', 'dial');
+ self.caller.session:setAutoHangup(false);
+
+ self.routes = common.configuration_file.get('/opt/freeswitch/scripts/ini/routes.ini');
+ self.caller.domain_local = self.domain;
+ self:retrieve_caller_data();
+
+ if not destination or destination.type == 'unknown' then
+ require 'dialplan.route'
+ local route = nil;
+
+ if self.caller.from_gateway then
+ local route_object = dialplan.route.Route:new{ log = self.log, database = self.database, routing_table = self.routes };
+ route = route_object:inbound(self.caller, self.caller.destination_number);
+ local inbound_caller_id_number = route_object:inbound_cid_number(self.caller, self.caller.gateway_name, 'gateway');
+ route_object.expandable.caller_id_number = inbound_caller_id_number;
+ local inbound_caller_id_name = route_object:inbound_cid_name(self.caller, self.caller.gateway_name, 'gateway');
+ self.log:info('INBOUND_CALLER_ID_REWRITE - number: ', inbound_caller_id_number, ', name: ', inbound_caller_id_name);
+ self.caller.caller_id_number = inbound_caller_id_number or self.caller.caller_id_number;
+ self.caller.caller_id_name = inbound_caller_id_name or self.caller.caller_id_name;
+ self.caller.caller_phone_numbers[1] = self.caller.caller_id_number;
+ else
+ route = dialplan.route.Route:new{ log = self.log, database = self.database, routing_table = self.routes }:prerouting(self.caller, self.caller.destination_number);
+ end
+
+ if route then
+ destination = self:destination_new{ number = route.value }
+ self.caller.destination_number = destination.number;
+ self.caller.destination = destination;
+ elseif not destination or destination.type == 'unknown' then
+ destination = self:destination_new{ number = self.caller.destination_number }
+ self.caller.destination = destination;
+ end
+ end
+
+ self.log:info('DIALPLAN start - caller_id: ',self.caller.caller_id_number, ' "', self.caller.caller_id_name,'"',
+ ', number: ', destination.number);
+
+ local result = { continue = false };
+ local loop = self.caller.loop_count;
+ while self.caller:ready() and loop < self.max_loops do
+ loop = loop + 1;
+ self.caller.loop_count = loop;
+
+ self.log:info('LOOP ', loop,
+ ' - destination: ', destination.type, '=', destination.id, '/', destination.uuid,'@', destination.node_id,
+ ', number: ', destination.number);
+
+ self.caller:set_variable('gs_destination_type', destination.type);
+ self.caller:set_variable('gs_destination_id', destination.id);
+ self.caller:set_variable('gs_destination_uuid', destination.uuid);
+ self.caller:set_variable('gs_destination_number', destination.number);
+ self.caller:set_variable('gs_destination_node_local', destination.node_local);
+
+ result = self:switch(destination);
+ result = result or { continue = false, code = 502, cause = 'DESTINATION_OUT_OF_ORDER', phrase = 'Destination out of order' }
+
+ if result.call_service then
+ self.caller:set_variable('gs_call_service', result.call_service);
+ end
+
+ if not result.continue then
+ break;
+ end
+
+ if result.call_forwarding then
+ self.log:info('LOOP ', loop, ' CALL_FORWARDING - service: ', result.call_forwarding.service,
+ ', destination: ', result.call_forwarding.type, '=', result.call_forwarding.id,
+ ', number: ', result.call_forwarding.number);
+
+ local auth_account = self:object_find(destination.type, destination.id);
+ self.caller.forwarding_number = destination.number;
+ self.caller.forwarding_service = result.call_forwarding.service;
+ self.caller:set_variable('gs_forwarding_service', self.caller.forwarding_service);
+ self.caller:set_variable('gs_forwarding_number', self.caller.forwarding_number);
+
+ if auth_account then
+ self.caller.auth_account = auth_account;
+ self.caller:set_auth_account(self.caller.auth_account);
+ self.log:info('AUTH_ACCOUNT_UPDATE - account: ', self.caller.auth_account.class, '=', self.caller.auth_account.id, '/', self.caller.auth_account.uuid);
+ if self.caller.auth_account.owner then
+ self.log:info('AUTH_ACCOUNT_UPDATE - auth owner: ', self.caller.auth_account.owner.class, '=', self.caller.auth_account.owner.id, '/', self.caller.auth_account.owner.uuid);
+ else
+ self.log:error('AUTH_ACCOUNT_UPDATE - auth owner not found');
+ end
+ end
+
+ destination = self:destination_new(result.call_forwarding);
+ self.caller.destination = destination;
+
+ if not result.no_cdr and auth_account then
+ require 'common.call_history'
+ common.call_history.CallHistory:new{ log = self.log, database = self.database }:insert_forwarded(
+ self.caller.uuid,
+ auth_account.class,
+ auth_account.id,
+ self.caller,
+ destination,
+ result
+ );
+ end
+ end
+
+ if result.number then
+ self.log:info('LOOP ', loop, ' NEW_DESTINATION_NUMBER - number: ', result.number );
+ destination = self:destination_new{ number = result.number }
+ self.caller.destination = destination;
+ end
+ end
+
+ if loop >= self.max_loops then
+ result = { continue = false, code = 483, cause = 'EXCHANGE_ROUTING_ERROR', phrase = 'Too many hops' }
+ end
+
+ self.log:info('DIALPLAN end - caller_id: ',self.caller.caller_id_number, ' "', self.caller.caller_id_name,'"',
+ ', destination: ', destination.type, '=', destination.id, '/', destination.uuid,'@', destination.node_id,
+ ', number: ', destination.number, ', result: ', result.code, ' ', result.phrase);
+
+ if self.caller:ready() then
+ self:hangup(result.code, result.phrase, result.cause);
+ end
+
+ self.caller:set_variable('gs_save_cdr', not result.no_cdr);
+end
diff --git a/misc/freeswitch/scripts/dialplan/fax.lua b/misc/freeswitch/scripts/dialplan/fax.lua
new file mode 100644
index 0000000..2a40620
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/fax.lua
@@ -0,0 +1,232 @@
+-- Gemeinschaft 5 module: fax class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+FAX_DOCUMENTS_DIRECTORY = '/tmp/'
+FAX_PARALLEL_MAX = 8;
+Fax = {}
+
+-- Create Fax object
+function Fax.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self)
+ self.__index = self
+ self.class = 'faxaccount';
+ self.log = arg.log;
+ self.database = arg.database;
+ self.record = arg.record;
+ self.fax_directory = arg.fax_directory or FAX_DOCUMENTS_DIRECTORY;
+ return object;
+end
+
+-- find fax account by id
+function Fax.find_by_id(self, id)
+ local sql_query = 'SELECT * FROM `fax_accounts` WHERE `id` = ' .. tonumber(id) .. ' LIMIT 1';
+ local fax_account = nil;
+
+ self.database:query(sql_query, function(fax_entry)
+ fax_account = Fax:new(self);
+ fax_account.record = fax_entry;
+ fax_account.id = tonumber(fax_entry.id);
+ fax_account.uuid = fax_entry.uuid;
+ end)
+
+ return fax_account;
+end
+
+
+-- find fax account by uuid
+function Fax.find_by_uuid(self, uuid)
+ local sql_query = 'SELECT * FROM `fax_accounts` WHERE `uuid` = "' .. uuid .. '" LIMIT 1';
+ local fax_account = nil;
+
+ self.database:query(sql_query, function(fax_entry)
+ fax_account = Fax:new(self);
+ fax_account.record = fax_entry;
+ fax_account.id = tonumber(fax_entry.id);
+ fax_account.uuid = fax_entry.uuid;
+ end)
+
+ return fax_account;
+end
+
+
+function Fax.destination_numbers(self, id)
+ local sql_query = 'SELECT `number` FROM `phone_numbers` WHERE `phone_numberable_type` = "FaxDocument" AND `phone_numberable_id` = ' .. tonumber(id);
+ local destination_numbers = {}
+
+ self.database:query(sql_query, function(fax_entry)
+ table.insert(destination_numbers, fax_entry.number);
+ end)
+
+ return destination_numbers;
+end
+
+function Fax.destination_number(self, id)
+ local sql_query = 'SELECT `number` FROM `phone_numbers` WHERE `phone_numberable_type` = "FaxDocument" AND `phone_numberable_id`= ' .. tonumber(id) .. ' LIMIT 1';
+ local destination_number = nil;
+
+ self.database:query(sql_query, function(fax_entry)
+ destination_number = fax_entry.number;
+ end)
+
+ return destination_number;
+end
+
+-- List waiting fax documents
+function Fax.queued_for_sending(self, limit)
+ limit = limit or FAX_PARALLEL_MAX;
+ local sql_query = 'SELECT * FROM `fax_documents` WHERE `state` IN ("queued_for_sending","unsuccessful") AND `retry_counter` > 0 ORDER BY `sent_at` ASC LIMIT ' .. limit;
+ local fax_documents = {}
+ self.database:query(sql_query, function(fax_entry)
+ fax_entry['destination_numbers'] = Fax:destination_numbers(fax_entry.id)
+ table.insert(fax_documents, fax_entry);
+ end)
+
+ return fax_documents;
+end
+
+-- Update fax document sending status
+function Fax.document_update(self, id, params)
+ require 'common.str'
+ local params_sql = {}
+
+ for name, value in pairs(params) do
+ table.insert(params_sql, '`' .. name .. '`=' .. common.str.to_sql(value));
+ end
+
+ if not params['sent_at'] then
+ table.insert(params_sql, '`sent_at`=NOW()');
+ end
+
+ if not params['updated_at'] then
+ table.insert(params_sql, '`updated_at`=NOW()');
+ end
+
+ local sql_query = 'UPDATE `fax_documents` SET ' .. table.concat(params_sql, ',') .. ' WHERE `id` = ' .. tonumber(id);
+
+ return self.database:query(sql_query);
+end
+
+
+function Fax.get_parameters(self, caller)
+ local fax_parameters = {
+ bad_rows = caller:to_i('fax_bad_rows'),
+ total_pages = caller:to_i('fax_document_total_pages'),
+ transferred_pages = caller:to_i('fax_document_transferred_pages'),
+ ecm_requested = caller:to_b('fax_ecm_requested'),
+ ecm_used = caller:to_b('fax_ecm_used'),
+ filename = caller:to_s('fax_filename'),
+ image_resolution = caller:to_s('fax_image_resolution'),
+ image_size = caller:to_i('fax_image_size'),
+ local_station_id = caller:to_s('fax_local_station_id'),
+ result_code = caller:to_i('fax_result_code'),
+ result_text = caller:to_s('fax_result_text'),
+ remote_station_id = caller:to_s('fax_remote_station_id'),
+ success = caller:to_b('fax_success'),
+ transfer_rate = caller:to_i('fax_transfer_rate'),
+ v17_disabled = caller:to_b('fax_v17_disabled'),
+ }
+
+ return fax_parameters;
+end
+
+-- Receive Fax
+function Fax.receive(self, caller, file_name)
+ file_name = file_name or self.fax_directory .. 'fax_in_' .. caller.uuid .. '.tiff';
+
+ caller:set_variable('fax_ident', self.record.station_id)
+ caller:set_variable('fax_verbose', 'false')
+
+ caller:answer();
+ local start_time = os.time();
+ caller:execute('rxfax', file_name);
+ local record = self:get_parameters(caller);
+ record.transmission_time = os.time() - start_time;
+ return record;
+end
+
+-- Send Fax
+function Fax.send(self, caller, file_name)
+ caller:set_variable('fax_ident', self.record.station_id)
+ caller:set_variable('fax_header', self.record.name)
+ caller:set_variable('fax_verbose', 'false')
+ local start_time = os.time();
+ caller:execute('txfax', file_name);
+ local record = self:get_parameters(caller);
+ record.transmission_time = os.time() - start_time;
+ return record;
+end
+
+-- find fax document by id
+function Fax.find_document_by_id(self, id)
+ local sql_query = 'SELECT * FROM `fax_documents` WHERE `id` = ' .. tonumber(id) .. ' LIMIT 1'
+ local record = nil
+
+ self.database:query(sql_query, function(fax_entry)
+ record = fax_entry;
+ end);
+
+ return record;
+end
+
+-- save fax document to database
+function Fax.insert_document(self, record)
+ require 'common.str'
+ local sql_query = 'INSERT INTO `fax_documents` ( \
+ inbound, \
+ retry_counter, \
+ fax_resolution_id, \
+ state, \
+ transmission_time, \
+ sent_at, \
+ document_total_pages, \
+ document_transferred_pages, \
+ ecm_requested, \
+ ecm_used, \
+ image_resolution, \
+ image_size, \
+ local_station_id, \
+ result_code, \
+ remote_station_id, \
+ success, \
+ transfer_rate, \
+ created_at, \
+ updated_at, \
+ fax_account_id, \
+ caller_id_number, \
+ caller_id_name, \
+ tiff, \
+ uuid \
+ ) VALUES ( \
+ true, \
+ 0, \
+ 1, \
+ "received", \
+ ' .. common.str.to_sql(record.transmission_time) .. ', \
+ NOW(), \
+ ' .. common.str.to_sql(record.total_pages) .. ', \
+ ' .. common.str.to_sql(record.transferred_pages) .. ', \
+ ' .. common.str.to_sql(record.ecm_requested) .. ', \
+ ' .. common.str.to_sql(record.ecm_used) .. ', \
+ ' .. common.str.to_sql(record.image_resolution) .. ', \
+ ' .. common.str.to_sql(record.image_size) .. ', \
+ ' .. common.str.to_sql(record.local_station_id) .. ', \
+ ' .. common.str.to_sql(record.result_code) .. ', \
+ ' .. common.str.to_sql(record.remote_station_id) .. ', \
+ ' .. common.str.to_sql(record.success) .. ', \
+ ' .. common.str.to_sql(record.transfer_rate) .. ', \
+ NOW(), \
+ NOW(), \
+ ' .. common.str.to_sql(self.id) .. ', \
+ ' .. common.str.to_sql(record.caller_id_number) .. ', \
+ ' .. common.str.to_sql(record.caller_id_name) .. ', \
+ ' .. common.str.to_sql(record.filename) .. ', \
+ ' .. common.str.to_sql(record.uuid) .. ' \
+ )';
+
+ return self.database:query(sql_query);
+end
diff --git a/misc/freeswitch/scripts/dialplan/functions.lua b/misc/freeswitch/scripts/dialplan/functions.lua
new file mode 100644
index 0000000..c104f89
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/functions.lua
@@ -0,0 +1,839 @@
+-- DialplanModule: Functions
+--
+module(...,package.seeall)
+
+Functions = {}
+
+-- Create Functions object
+function Functions.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self)
+ self.__index = self
+ self.log = arg.log
+ self.database = arg.database
+ self.domain = arg.domain
+ return object
+end
+
+function Functions.ensure_caller_sip_account(self, caller)
+ if caller.account and caller.account.class == 'sipaccount' then
+ return caller.account;
+ end
+end
+
+function Functions.dialplan_function(self, caller, dialed_number)
+ require 'common.str'
+ local parameters = common.str.to_a(dialed_number, '%-');
+ if not parameters[2] then
+ return { continue = false, code = 484, phrase = 'Malformed function parameters', no_cdr = true };
+ end
+ local fid = tostring(parameters[2]);
+ local result = { continue = false, code = 404, phrase = 'Function not found', no_cdr = true };
+
+ self.log:debug('DIALPLAN_DUNCTION - execute: ', dialed_number);
+
+ if fid == "ta" then
+ result = self:transfer_all(caller, parameters[3]);
+ elseif fid == "in" then
+ result = self:intercept_extensions(caller, parameters[3]);
+ elseif fid == "ia" then
+ result = self:intercept_any_extension(caller, parameters[3]);
+ elseif fid == "anc" then
+ result = self:account_node_change(caller);
+ elseif fid == "li" then
+ result = self:user_login(caller, parameters[3], parameters[4]);
+ elseif fid == "lo" then
+ result = self:user_logout(caller);
+ elseif fid == "lir" then
+ result = self:user_login_redirect(caller, parameters[3], parameters[4]);
+ elseif fid == "loaon" then
+ result = self:user_auto_logout(caller, true);
+ elseif fid == "loaoff" then
+ result = self:user_auto_logout(caller, false);
+ elseif fid == "dcliroff" then
+ result = self:dial_clir_off(caller, parameters[3]);
+ elseif fid == "dcliron" then
+ result = self:dial_clir_on(caller, parameters[3]);
+ elseif fid == "clipon" then
+ result = self:clip_on(caller);
+ elseif fid == "clipoff" then
+ result = self:clip_off(caller);
+ elseif fid == "cwaoff" then
+ result = self:callwaiting_off(caller);
+ elseif fid == "cwaon" then
+ result = self:callwaiting_on(caller);
+ elseif fid == "cfoff" then
+ result = self:call_forwarding_off(caller);
+ elseif fid == "cfdel" then
+ result = self:call_forwarding_off(caller, nil, true);
+ elseif fid == "cfu" then
+ result = self:call_forwarding_on(caller, 'always', parameters[3], 'PhoneNumber');
+ elseif fid == "cfuoff" then
+ result = self:call_forwarding_off(caller, 'always');
+ elseif fid == "cfudel" then
+ result = self:call_forwarding_off(caller, 'always', true);
+ elseif fid == "cfutg" then
+ result = self:call_forwarding_toggle(caller, 'always', parameters[3]);
+ elseif fid == "cfn" then
+ result = self:call_forwarding_on(caller, 'noanswer', parameters[3], 'PhoneNumber', parameters[4]);
+ elseif fid == "cfnoff" then
+ result = self:call_forwarding_off(caller, 'noanswer');
+ elseif fid == "cfndel" then
+ result = self:call_forwarding_off(caller, 'noanswer', true);
+ elseif fid == "cfo" then
+ result = self:call_forwarding_on(caller, 'offline', parameters[3], 'PhoneNumber');
+ elseif fid == "cfooff" then
+ result = self:call_forwarding_off(caller, 'offline');
+ elseif fid == "cfodel" then
+ result = self:call_forwarding_off(caller, 'offline', true);
+ elseif fid == "cfb" then
+ result = self:call_forwarding_on(caller, 'busy', parameters[3], 'PhoneNumber');
+ elseif fid == "cfboff" then
+ result = self:call_forwarding_off(caller, 'busy');
+ elseif fid == "cfbdel" then
+ result = self:call_forwarding_off(caller, 'busy', true);
+ elseif fid == "vmleave" then
+ result = self:voicemail_message_leave(caller, parameters[3]);
+ elseif fid == "vmcheck" then
+ result = self:voicemail_check(caller, parameters[3]);
+ elseif fid == "vmtg" then
+ result = self:call_forwarding_toggle(caller, nil, parameters[3]);
+ elseif fid == "acdmtg" then
+ result = self:acd_membership_toggle(caller, parameters[3], parameters[4]);
+ elseif fid == "e164" then
+ result = "+" .. tostring(parameters[3]);
+ elseif fid == "hangup" then
+ result = self:hangup(caller, parameters[3], parameters[4]);
+ end
+
+ return result;
+end
+
+-- Transfer all calls to a conference
+function Functions.transfer_all(self, caller, destination_number)
+ self.log:info('TRANSFER_ALL - caller: ', caller.account_type, '/', caller.account_uuid, ' number: ', destination_number);
+
+ local caller_sip_account = self:ensure_caller_sip_account(caller);
+ if not caller_sip_account then
+ self.log:error('TRANSFER_ALL - incompatible caller');
+ return { continue = false, code = 403, phrase = 'Incompatible caller' }
+ end
+
+ -- Query call and channel table for channel IDs
+ local sql_query = 'SELECT `b`.`name` AS `caller_chan_name`, `a`.`caller_uuid`, `a`.`callee_uuid` \
+ FROM `calls` `a` JOIN `channels` `b` ON `a`.`caller_uuid` = `b`.`uuid` JOIN `channels` `c` \
+ ON `a`.`callee_uuid` = `c`.`uuid` WHERE `b`.`name` LIKE ("%' .. caller_sip_account.record.auth_name .. '@%") \
+ OR `c`.`name` LIKE ("%' .. caller_sip_account.record.auth_name .. '@%") LIMIT 100';
+
+ self.database:query(sql_query, function(call_entry)
+ local uid = nil
+ if call_entry.caller_chan_name:find(caller_sip_account.record.auth_name .. "@") then
+ uid = call_entry.callee_uuid;
+ self.log:debug("Transfering callee channel with uid: " .. uid);
+ else
+ uid = call_entry.caller_uuid;
+ self.log:debug("Transfering caller channel with uid: " .. uid);
+ end
+ freeswitch.API():execute("uuid_transfer", uid .. " " .. destination_number);
+ end)
+
+ return destination_number;
+end
+
+-- Intercept Extensions
+function Functions.intercept_extensions(self, caller, destination_numbers)
+ if type(destination_numbers) == "string" then
+ destination_numbers = "\"" .. destination_numbers .. "\"";
+ else
+ destination_numbers = "\"" .. table.concat(destination_numbers, "\",\"") .. "\"";
+ end
+
+ self.log:debug("Intercept call to number(s): " .. destination_numbers);
+
+ if caller.account_type ~= "SipAccount" then
+ self.log:error("caller is not a SipAccount");
+ return { continue = false, code = 403, phrase = 'Incompatible caller' }
+ end
+
+ local sql_query = 'SELECT * FROM `channels` WHERE `callstate` IN ("EARLY", "ACTIVE") AND `dest` IN (' .. destination_numbers .. ') LIMIT 1';
+
+ self.database:query(sql_query, function(call_entry)
+ self.log:debug("intercepting call with uid: " .. call_entry.uuid);
+ caller:intercept(call_entry.uuid);
+ end)
+
+ return nil;
+end
+
+-- intercept call to destination (e.g. sip_account)
+function Functions.intercept_destination(self, caller, destination)
+ self.log:debug("Intercept call to destination " .. destination);
+ local result = false;
+ local sql_query = 'SELECT `call_uuid`, `uuid` FROM `channels` WHERE `callstate` = "RINGING" AND `dest` = "' .. destination .. '" LIMIT 1';
+
+ caller:set_caller_id(caller.caller_phone_numbers[1] ,caller.caller_id_name);
+ self.database:query(sql_query, function(call_entry)
+ if call_entry.call_uuid and tostring(call_entry.call_uuid) then
+ self.log:debug("intercepting call - uuid: " .. call_entry.call_uuid);
+ caller:intercept(call_entry.call_uuid);
+ result = { continue = false, code = 200, call_service = 'pickup' }
+ require 'common.str'
+ require 'common.fapi'
+ local fapi = common.fapi.FApi:new{ log = self.log, uuid = call_entry.call_uuid }
+ if fapi:channel_exists() then
+ caller:set_caller_id(
+ common.str.to_s(fapi:get_variable('effective_caller_id_number')),
+ common.str.to_s(fapi:get_variable('effective_caller_id_name'))
+ );
+ caller:set_callee_id(
+ common.str.to_s(fapi:get_variable('effective_callee_id_number')),
+ common.str.to_s(fapi:get_variable('effective_callee_id_name'))
+ );
+
+ caller:set_variable('gs_destination_type', fapi:get_variable('gs_destination_type'));
+ caller:set_variable('gs_destination_id', fapi:get_variable('gs_destination_id'));
+ caller:set_variable('gs_destination_uuid', fapi:get_variable('gs_destination_uuid'));
+
+ caller:set_variable('gs_caller_account_type', fapi:get_variable('gs_account_type'));
+ caller:set_variable('gs_caller_account_id', fapi:get_variable('gs_account_id'));
+ caller:set_variable('gs_caller_account_uuid', fapi:get_variable('gs_account_uuid'));
+
+ caller:set_variable('gs_auth_account_type', fapi:get_variable('gs_auth_account_type'));
+ caller:set_variable('gs_auth_account_id', fapi:get_variable('gs_auth_account_id'));
+ caller:set_variable('gs_auth_account_uuid', fapi:get_variable('gs_auth_account_uuid'));
+ end
+ else
+ self.log:error('FUNCTION - failed to intercept call - no caller uuid for callee uuid: ', call_entry.uuid);
+ end
+ end)
+
+ return result;
+end
+
+-- intercept call to owner of destination_number
+function Functions.intercept_any_extension(self, caller, destination_number)
+ require 'common.phone_number'
+ local phone_number_object = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database }:find_by_number(destination_number);
+
+ if not phone_number_object or not phone_number_object.record then
+ self.log:notice("unallocated number: " .. tostring(destination_number));
+ return false;
+ end
+
+ if phone_number_object.record.phone_numberable_type == 'SipAccount' then
+ require "common.sip_account"
+ local sip_account_class = common.sip_account.SipAccount:new{ log = self.log, database = self.database }
+ local sip_account = sip_account_class:find_by_id(phone_number_object.record.phone_numberable_id)
+ if sip_account then
+ return self:intercept_destination(caller, sip_account.record.auth_name);
+ end
+ end
+end
+
+
+function Functions.account_node_change(self, caller)
+ self.log:info('NODE_CHANGE - caller: ', caller.account_type, '/', caller.account_uuid, ', caller_id: ', caller.caller_id_number);
+
+ -- find caller's sip account
+ local caller_sip_account = caller.account;
+ if not caller_sip_account or not caller_sip_account.class == 'sipaccount' then
+ self.log:notice('LOGIN - caller sip_account not found');
+ return { continue = false, code = 404, phrase = 'Account not found', no_cdr = true }
+ end
+
+ require 'phones.phone'
+ local phone_class = phones.phone.Phone:new{log = self.log, database = self.database}
+
+ -- logout caller phones if caller account is hot-deskable
+ local caller_phones = phone_class:find_all_hot_deskable_by_account(caller_sip_account.record.id);
+ for index, phone_caller in ipairs(caller_phones) do
+ phone_caller:logout(caller_sip_account.record.id);
+ end
+
+ self:update_node_change(caller_sip_account, caller.local_node_id);
+ caller:answer();
+ caller:send_display('Change successful');
+ caller.session:sayPhrase('logged_in');
+
+ -- resync caller phones
+ for index, phone_caller in ipairs(caller_phones) do
+ local result = phone_caller:resync{ auth_name = caller_sip_account.auth_name, domain = caller.domain };
+ self.log:info('NODE_CHANGE - resync phone - mac: ', phone_caller.record.mac_address, ', ip_address: ', phone_caller.record.ip_address, ', result: ', result);
+ end
+
+ return { continue = false, code = 200, phrase = 'OK', no_cdr = true }
+end
+
+
+function Functions.user_login(self, caller, number, pin)
+ require 'common.str'
+
+ local PHONE_NUMBER_LEN_MIN = 4;
+ local PHONE_NUMBER_LEN_MAX = 12;
+ local PIN_LEN_MIN = 4;
+ local PIN_LEN_MAX = 12;
+
+ caller:set_variable('destination_number', 'f-li-' .. common.str.to_s(number) .. '-PIN');
+ self.log:info('LOGIN - caller: ', caller.account_type, '/', caller.account_uuid, ', caller_id: ', caller.caller_id_number, ', number: ', number);
+
+ if common.str.blank(number) then
+ number = caller.session:read(PHONE_NUMBER_LEN_MIN, PHONE_NUMBER_LEN_MAX, 'ivr/ivr-please_enter_extension_followed_by_pound.wav', 3000, '#');
+ end
+
+ -- find caller's sip account
+ local caller_sip_account = caller.account;
+ if not caller_sip_account or not caller_sip_account.class == 'sipaccount' then
+ self.log:notice('LOGIN - caller sip_account not found');
+ return { continue = false, code = 404, phrase = 'Caller not found', no_cdr = true }
+ end
+
+ require 'phones.phone'
+ local phone_class = phones.phone.Phone:new{log = self.log, database = self.database}
+
+ local caller_phones = phone_class:find_all_hot_deskable_by_account(caller_sip_account.id);
+ local caller_phone = caller_phones[1];
+
+ if not caller_phone then
+ self.log:notice('LOGIN - caller phone not found or not hot-deskable');
+ return { continue = false, code = 403, phrase = 'Phone not hot-deskable', no_cdr = true }
+ end
+
+ require 'common.phone_number'
+ local phone_number = common.phone_number.PhoneNumber:new{log = self.log, database = self.database}:find_by_number(number, {"SipAccount"});
+
+ if not phone_number then
+ self.log:notice('LOGIN - number not found or not linked to a sip account - number: ', number);
+ return { continue = false, code = 404, phrase = 'Account not found', no_cdr = true }
+ end
+
+ require 'common.sip_account'
+ local destination_sip_account = common.sip_account.SipAccount:new{ log = self.log, database = self.database }:find_by_id(phone_number.record.phone_numberable_id);
+
+ if not destination_sip_account then
+ self.log:notice('LOGIN - account not found - ', phone_number.record.phone_numberable_type, '=', phone_number.record.phone_numberable_id, ', number: ', number);
+ return { continue = false, code = 404, phrase = 'Account not found', no_cdr = true }
+ end
+
+ self.log:info('LOGIN - destination: ', phone_number.record.phone_numberable_type, '=', destination_sip_account.record.id,
+ ', caller_name: ', destination_sip_account.record.caller_name, ', hotdeskable: ', destination_sip_account.record.hotdeskable);
+
+ if not common.str.to_b(destination_sip_account.record.hotdeskable) then
+ self.log:notice('LOGIN - destination sip_account not hot-deskable');
+ return { continue = false, code = 404, phrase = 'Destination not hot-deskable', no_cdr = true }
+ end
+
+ require 'dialplan.user'
+ local user = dialplan.user.User:new{ log = self.log, database = self.database }:find_by_id(destination_sip_account.record.sip_accountable_id);
+
+ if common.str.blank(pin) then
+ pin = caller.session:read(PIN_LEN_MIN, PIN_LEN_MAX, 'ivr/ivr-please_enter_pin_followed_by_pound.wav', 3000, '#');
+ end
+
+ if not user then
+ self.log:notice('LOGIN - user not found - ', destination_sip_account.record.sip_accountable_type, '=',destination_sip_account.record.sip_accountable_id);
+ return { continue = false, code = 403, phrase = 'Authentication failed', no_cdr = true }
+ end
+
+ if not user:check_pin(pin) then
+ self.log:notice('LOGIN - authentication failed');
+ return { continue = false, code = 403, phrase = 'Authentication failed', no_cdr = true }
+ end
+
+ -- logout caller phones if caller account is hot-deskable
+ if common.str.to_b(caller_sip_account.record.hotdeskable) then
+ for index, phone_caller in ipairs(caller_phones) do
+ phone_caller:logout(caller_sip_account.record.id);
+ end
+ end
+
+ local destination_phones = phone_class:find_all_hot_deskable_by_account(destination_sip_account.record.id);
+ -- logout destination phones
+ for index, phone_destination in ipairs(destination_phones) do
+ phone_destination:logout(destination_sip_account.record.id);
+ end
+
+ local result = caller_phone:login(destination_sip_account.record.id, destination_sip_account.record.sip_accountable_id, destination_sip_account.record.sip_accountable_type);
+ self.log:info('LOGIN - account login - mac: ', caller_phone.record.mac_address, ', ip_address: ', caller_phone.record.ip_address, ', result: ', result);
+
+ if not result then
+ return { continue = false, code = 403, phrase = 'Login failed', no_cdr = true }
+ end
+
+ caller:answer();
+ caller:send_display('Login successful');
+
+ self:update_node_change(destination_sip_account, caller.local_node_id);
+ caller:sleep(1000);
+
+ -- resync destination phones
+ for index, phone_destination in ipairs(destination_phones) do
+ local result = phone_destination:resync{ auth_name = destination_sip_account.auth_name, domain = caller.domain_local };
+ self.log:info('LOGIN - resync destination phone - mac: ', phone_destination.record.mac_address, ', ip_address: ', phone_destination.record.ip_address, ', result: ', result);
+ end
+
+ -- resync caller phones
+ for index, phone_caller in ipairs(caller_phones) do
+ local result = phone_caller:resync{ auth_name = caller_sip_account.auth_name, domain = caller.domain };
+ self.log:info('LOGIN - resync caller phone - mac: ', phone_caller.record.mac_address, ', ip_address: ', phone_caller.record.ip_address, ', result: ', result);
+ end
+
+ return { continue = false, code = 200, phrase = 'OK', no_cdr = true }
+end
+
+
+function Functions.user_logout(self, caller)
+ require 'common.str'
+ self.log:info('LOGOUT - caller: ', caller.account_type, '/', caller.account_uuid, ', caller_id: ', caller.caller_id_number);
+
+ -- find caller's sip account
+ local caller_sip_account = caller.account;
+ if not caller_sip_account or not caller_sip_account.class == 'sipaccount' then
+ self.log:notice('LOGOUT - caller sip_account not found');
+ return { continue = false, code = 404, phrase = 'Caller not found', no_cdr = true }
+ end
+
+ if not common.str.to_b(caller_sip_account.record.hotdeskable) then
+ self.log:notice('LOGOUT - caller sip_account not hot-deskable');
+ return { continue = false, code = 404, phrase = 'Caller not hot-deskable', no_cdr = true }
+ end
+
+ require 'phones.phone'
+ local phone_class = phones.phone.Phone:new{log = self.log, database = self.database}
+
+ local caller_phones = phone_class:find_all_hot_deskable_by_account(caller_sip_account.id);
+
+ if caller_phones == 0 then
+ self.log:notice('LOGOUT - caller phones not found or not hot-deskable');
+ return { continue = false, code = 403, phrase = 'Phone not hot-deskable', no_cdr = true }
+ end
+
+ local result = false;
+ for index, phone_caller in ipairs(caller_phones) do
+ result = phone_caller:logout(caller_sip_account.record.id);
+ self.log:info('LOGOUT - account logout - mac: ', phone_caller.record.mac_address, ', ip_address: ', phone_caller.record.ip_address, ', result: ', result);
+ end
+
+ caller:answer();
+ caller:send_display('Logout successful');
+ caller:sleep(1000);
+
+ -- resync caller phones
+ for index, phone_caller in ipairs(caller_phones) do
+ local result = phone_caller:resync{ auth_name = caller_sip_account.auth_name, domain = caller.domain };
+ self.log:info('LOGIN - resync caller phone - mac: ', phone_caller.record.mac_address, ', ip_address: ', phone_caller.record.ip_address, ', result: ', result);
+ end
+
+ return { continue = false, code = 200, phrase = 'OK', no_cdr = true }
+end
+
+
+function Functions.update_node_change(self, sip_account, node_id)
+ require 'common.sync_log'
+ local sync_log_class = common.sync_log.SyncLog:new{ log = self.log, database = self.database, homebase_ip_address = sip_account.record.host }
+
+ if tostring(sip_account.record.gs_node_id) ~= tostring(node_id) then
+ self.log:info('UPDATE_NODE - from: ', sip_account.record.gs_node_id, ', to: ', node_id, ', sipaccount=', sip_account.record.id, '/', sip_account.record.uuid, '@', node_id, ', caller_name: ', sip_account.record.caller_name);
+ sql_query = 'UPDATE `sip_accounts` SET `updated_at` = NOW(), `gs_node_id` = ' .. tonumber(node_id) .. ' WHERE id = ' .. tonumber(sip_account.record.id);
+ if self.database:query(sql_query) then
+ sync_log_class:insert('SipAccount', { uuid = sip_account.record.uuid, gs_node_id = tonumber(node_id), updated_at = os.date('!%Y-%m-%d %H:%M:%S %Z') }, 'update', { 'gs_node_id', 'updated_at' });
+ end
+ end
+
+ require 'common.phone_number'
+ local phone_numbers = common.phone_number.PhoneNumber:new{log = self.log, database = self.database}:find_all_by_owner(sip_account.record.id, 'SipAccount');
+ for number_id, phone_number in pairs(phone_numbers) do
+ if tostring(phone_number.record.gs_node_id) ~= tostring(node_id) then
+ self.log:info('UPDATE_NODE - from: ', phone_number.record.gs_node_id, ', to: ', node_id, ', phonenumber=', phone_number.record.id, '/', phone_number.record.uuid, '@', node_id, ', number: ', phone_number.record.number);
+ sql_query = 'UPDATE `phone_numbers` SET `updated_at` = NOW(), `gs_node_id` = ' .. tonumber(node_id) .. ' WHERE id = ' .. tonumber(number_id);
+
+ if self.database:query(sql_query) then
+ sync_log_class:insert('PhoneNumber', { uuid = phone_number.record.uuid, gs_node_id = tonumber(node_id), updated_at = os.date('!%Y-%m-%d %H:%M:%S %Z') }, 'update', { 'gs_node_id', 'updated_at' });
+ end
+ end
+ end
+end
+
+
+function Functions.user_login_redirect(self, caller, phone_number, pin)
+ -- Remove PIN from destination_number
+ caller.session:setVariable("destination_number", "f-li-" .. tostring(phone_number) .. "-PIN");
+
+ -- Redirect to f-li function
+ caller.session:execute("redirect", "sip:f-li-" .. tostring(phone_number) .. "-" .. tostring(pin) .. "@" .. caller.domain);
+end
+
+-- Set nightly_reboot flag
+function Functions.user_auto_logout(self, caller, auto_logout)
+ local nightly_reboot = 'FALSE';
+ if auto_logout then
+ nightly_reboot = 'TRUE';
+ end
+
+ -- Ensure a valid sip account
+ local caller_sip_account = caller.account;
+ if not caller_sip_account or not caller_sip_account.class == 'sipaccount' then
+ return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true }
+ end
+
+ require "phones.phone"
+ local phone_class = phones.phone.Phone:new{log = self.log, database = self.database}
+
+ -- Get caller phone
+ local caller_phone = phone_class:find_hot_deskable_by_account(caller_sip_account.id);
+ if not caller_phone then
+ self.log:notice("Caller phone not found or not hot-deskable");
+ return { continue = false, code = 401, phrase = 'Phone not hot-deskable', no_cdr = true }
+ end
+
+ log:debug("Hot-desking auto log off - caller phone: " .. caller_phone.record.id .. ", mac: " .. caller_phone.record.mac_address);
+
+ sql_query = 'UPDATE `phones` SET `nightly_reboot` = ' .. nightly_reboot .. ' WHERE `id` = ' .. tonumber(caller_phone.record.id);
+
+ if not self.database:query(sql_query) then
+ self.log:error('Hot-desking auto log off status could not be changed from ' .. tostring(caller_phone.record.nightly_reboot) .. ' to ' .. nightly_reboot);
+ return { continue = false, code = 401, phrase = 'Value could not be changed', no_cdr = true }
+
+ end
+
+ self.log:debug('Hot-desking auto log off changed from ' .. tostring(caller_phone.record.nightly_reboot) .. ' to ' .. nightly_reboot);
+
+ caller:answer();
+ caller:send_display('Logout successful');
+ caller:sleep(1000);
+end
+
+function Functions.dial_clir_off(self, caller, phone_number)
+ -- Ensure a valid sip account
+ local caller_sip_account = self:ensure_caller_sip_account(caller);
+ if not caller_sip_account then
+ return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true }
+ end
+
+ caller.clir = false;
+ return { continue = true, number = phone_number }
+end
+
+function Functions.dial_clir_on(self, caller, phone_number)
+ -- Ensure a valid sip account
+ local caller_sip_account = self:ensure_caller_sip_account(caller);
+ if not caller_sip_account then
+ return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true }
+ end
+
+ caller.clir = true;
+ return { continue = true, number = phone_number }
+end
+
+function Functions.callwaiting_on(self, caller)
+ -- Find caller's SipAccount
+ local caller_sip_account = self:ensure_caller_sip_account(caller);
+ if not caller_sip_account then
+ return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true }
+ end
+
+ local sql_query = 'UPDATE `sip_accounts` SET `call_waiting` = TRUE WHERE `id` = ' .. caller_sip_account.record.id;
+
+ if not self.database:query(sql_query) then
+ self.log:notice("Call Waiting could not be set");
+ return { continue = false, code = 500, phrase = 'Call Waiting could not be set', no_cdr = true }
+ end
+
+ caller:answer();
+ caller:send_display('Call waiting on');
+ caller:sleep(1000);
+ return { continue = false, code = 200, phrase = 'OK', no_cdr = true }
+end
+
+function Functions.callwaiting_off(self, caller)
+ -- Find caller's SipAccount
+ local caller_sip_account = self:ensure_caller_sip_account(caller);
+ if not caller_sip_account then
+ return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true }
+ end
+
+ local sql_query = 'UPDATE `sip_accounts` SET `call_waiting` = FALSE WHERE `id` = ' .. caller_sip_account.record.id;
+
+ if not self.database:query(sql_query) then
+ self.log:notice("Call Waiting could not be set");
+ return { continue = false, code = 500, phrase = 'Call Waiting could not be set', no_cdr = true }
+ end
+
+ caller:answer();
+ caller:send_display('Call waiting off');
+ caller:sleep(1000);
+ return { continue = false, code = 200, phrase = 'OK', no_cdr = true }
+end
+
+function Functions.clip_on(self, caller)
+ -- Find caller's SipAccount
+ local caller_sip_account = self:ensure_caller_sip_account(caller);
+ if not caller_sip_account then
+ return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true }
+ end
+
+ local sql_query = 'UPDATE `sip_accounts` SET `clip` = TRUE WHERE `id` = ' .. caller_sip_account.record.id;
+
+ if not self.database:query(sql_query) then
+ self.log:notice("CLIP could not be set");
+ return { continue = false, code = 500, phrase = 'CLIP could not be set', no_cdr = true }
+
+ end
+
+ caller:answer();
+ caller:send_display('CLIP on');
+ caller:sleep(1000);
+ return { continue = false, code = 200, phrase = 'OK', no_cdr = true }
+end
+
+function Functions.clip_off(self, caller)
+ -- Find caller's SipAccount
+ local caller_sip_account = self:ensure_caller_sip_account(caller);
+ if not caller_sip_account then
+ return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true }
+ end
+
+ local sql_query = 'UPDATE `sip_accounts` SET `clip` = FALSE WHERE `id` = ' .. caller_sip_account.record.id;
+
+ if not self.database:query(sql_query) then
+ self.log:notice("CLIP could not be set");
+ return { continue = false, code = 500, phrase = 'CLIP could not be set', no_cdr = true }
+
+ end
+
+ caller:answer();
+ caller:send_display('CLIP off');
+ caller:sleep(1000);
+ return { continue = false, code = 200, phrase = 'OK', no_cdr = true }
+end
+
+
+function Functions.call_forwarding_off(self, caller, call_forwarding_service, delete)
+ local defaults = {log = self.log, database = self.database, domain = caller.domain}
+
+ -- Find caller's SipAccount
+ local caller_sip_account = self:ensure_caller_sip_account(caller);
+ if not caller_sip_account then
+ return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true }
+ end
+
+ require 'common.phone_number'
+ local phone_number_class = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database, domain = caller.domain };
+ local phone_numbers = phone_number_class:list_by_owner(caller_sip_account.record.id, 'SipAccount');
+
+ local success = false;
+ for index, phone_number in pairs(phone_numbers) do
+ phone_number_object = phone_number_class:find_by_number(phone_number);
+ if phone_number_object then
+ if phone_number_object:call_forwarding_off(call_forwarding_service, nil, delete) then
+ success = true;
+ end
+ end
+ end
+
+ if not success then
+ self.log:notice("call forwarding could not be deactivated");
+ return { continue = false, code = 500, phrase = 'Call Forwarding could not be deactivated', no_cdr = true }
+
+ end
+
+ caller:answer();
+ caller:send_display('Call forwarding off');
+ caller:sleep(1000);
+ return { continue = false, code = 200, phrase = 'OK', no_cdr = true }
+end
+
+
+function Functions.call_forwarding_on(self, caller, call_forwarding_service, destination, destination_type, timeout)
+ local defaults = {log = self.log, database = self.database, domain = caller.domain}
+
+ if not call_forwarding_service then
+ self.log:notice('no call forwarding service specified');
+ end
+
+ -- Find caller's SipAccount
+ local caller_sip_account = self:ensure_caller_sip_account(caller);
+ if not caller_sip_account then
+ return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true }
+ end
+
+ require "common.phone_number"
+ local phone_number_class = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database, domain = caller.domain };
+ local phone_numbers = phone_number_class:list_by_owner(caller_sip_account.record.id, 'SipAccount');
+
+ local success = false;
+ for index, phone_number in pairs(phone_numbers) do
+ phone_number_object = phone_number_class:find_by_number(phone_number);
+ if phone_number_object then
+ if phone_number_object:call_forwarding_on(call_forwarding_service, destination, timeout) then
+ success = true;
+ end
+ end
+ end
+
+ if not success then
+ self.log:notice("call forwarding could not be activated");
+ return { continue = false, code = 500, phrase = 'Call Forwarding could not be activated', no_cdr = true }
+
+ end
+
+ caller:answer();
+ caller:send_display('Call forwarding on');
+ caller:sleep(1000);
+ return { continue = false, code = 200, phrase = 'OK', no_cdr = true }
+end
+
+
+function Functions.call_forwarding_toggle(self, caller, call_forwarding_service, phone_number_id)
+ local defaults = {log = self.log, database = self.database, domain = caller.domain}
+
+ -- Find caller's SipAccount
+ local caller_sip_account = self:ensure_caller_sip_account(caller);
+ if not caller_sip_account then
+ return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true }
+ end
+
+ require "common.phone_number"
+ local phone_number_class = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database, domain = caller.domain };
+ local phone_numbers = phone_number_class:list_by_owner(caller_sip_account.record.id, 'SipAccount');
+
+ local result = nil;
+ for index, phone_number in pairs(phone_numbers) do
+ phone_number_object = phone_number_class:find_by_number(phone_number);
+ if phone_number_object then
+ if not result then
+ result = phone_number_object:call_forwarding_toggle(call_forwarding_service);
+ elseif result.active then
+ phone_number_object:call_forwarding_on(call_forwarding_service, result.destination, result.destination_type, result.timeout);
+ else
+ phone_number_object:call_forwarding_off(call_forwarding_service);
+ end
+ end
+ end
+
+ if not result then
+ self.log:notice("call forwarding could not be toggled");
+ return { continue = false, code = 500, phrase = 'Call Forwarding could not be toggled', no_cdr = true }
+
+ end
+
+ caller:answer();
+ caller:send_display('Call forwarding toggled');
+ caller:sleep(1000);
+ return { continue = false, code = 200, phrase = 'OK', no_cdr = true }
+end
+
+
+function Functions.voicemail_message_leave(self, caller, phone_number)
+ require 'dialplan.voicemail'
+ local voicemail_account = dialplan.voicemail.Voicemail:new{ log = self.log, database = self.database }:find_by_number(phone_number);
+
+ if not voicemail_account then
+ return { continue = false, code = 404, phrase = 'Mailbox not found', no_cdr = true }
+ end
+
+ voicemail_account:leave(caller, phone_number);
+
+ if caller:to_s("voicemail_message_len") ~= '' then
+ voicemail_account:send_notify(caller);
+ else
+ self.log:debug("voicemail - no message saved");
+ end
+
+ return { continue = false, code = 200, phrase = 'OK' }
+end
+
+
+function Functions.voicemail_check(self, caller, phone_number)
+ local voicemail_account = nil;
+ local voicemail_authorized = false;
+
+ require 'dialplan.voicemail'
+
+ if phone_number then
+ voicemail_account = dialplan.voicemail.Voicemail:new{ log = self.log, database = self.database }:find_by_number(phone_number);
+ else
+ if caller.auth_account_type == 'SipAccount' then
+ voicemail_account = dialplan.voicemail.Voicemail:new{ log = self.log, database = self.database }:find_by_sip_account_id(caller.auth_account.id);
+ voicemail_authorized = true;
+ end
+ end
+
+ if not voicemail_account then
+ return { continue = false, code = 404, phrase = 'Mailbox not found', no_cdr = true }
+ end
+
+ voicemail_account:menu(caller, voicemail_authorized);
+
+ return { continue = false, code = 200, phrase = 'OK', no_cdr = true }
+end
+
+
+function Functions.acd_membership_toggle(self, caller, agent_id, phone_number)
+ -- Find caller's SipAccount
+ local caller_sip_account = self:ensure_caller_sip_account(caller);
+ if not caller_sip_account then
+ return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true }
+ end
+
+ require 'dialplan.acd'
+ local acd_class = dialplan.acd.AutomaticCallDistributor:new{ log = self.log, database = self.database, domain = self.domain };
+
+ self.log:info('ACD_MEMBERSHIP_TOGGLE - sipaccount=', caller_sip_account.id, '/', caller_sip_account.uuid, ', agent=', agent_id, ', ACD phone number: ', phone_number);
+
+ if not tonumber(agent_id) or tonumber(agent_id) == 0 then
+
+ if not phone_number then
+ self.log:notice('ACD_MEMBERSHIP_TOGGLE - neither agent_id nor phone_number specified');
+ return { continue = false, code = 404, phrase = 'Agent not found', no_cdr = true }
+ end
+
+ require "common.phone_number"
+ local phone_number_object = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database, domain = caller.domain }:find_by_number(phone_number, {'AutomaticCallDistributor'});
+
+ if not phone_number_object or not tonumber(phone_number_object.record.phone_numberable_id) then
+ self.log:notice('ACD_MEMBERSHIP_TOGGLE - ACD not found');
+ return { continue = false, code = 404, phrase = 'ACD not found', no_cdr = true }
+ end
+
+ local agent = acd_class:agent_find_by_acd_and_destination(phone_number_object.record.phone_numberable_id, caller_sip_account.class, caller_sip_account.id);
+
+ if not agent or not tonumber(agent.id) then
+ self.log:notice('ACD_MEMBERSHIP_TOGGLE - agent not found');
+ return { continue = false, code = 404, phrase = 'Agent not found', no_cdr = true }
+ end
+
+ agent_id = agent.id;
+ end
+
+ local status = acd_class:agent_status_toggle(agent_id, 'sipaccount', caller_sip_account.id);
+
+ if not status then
+ self.log:error('ACD_MEMBERSHIP_TOGGLE - error toggling ACD membership');
+ return { continue = false, code = 500, phrase = 'Error toggling ACD membership', no_cdr = true }
+ end
+
+ self.log:info('ACD_MEMBERSHIP_TOGGLE - sipaccount=', caller_sip_account.id, '/', caller_sip_account.uuid, ', agent=', agent_id, ', status: ', status);
+
+ caller:answer();
+ caller:send_display('ACD membership toggled: ' .. status);
+ caller:sleep(500);
+ caller.session:sayPhrase('acd_agent_status', tostring(status));
+ return { continue = false, code = 200, phrase = 'OK', no_cdr = true }
+end
+
+function Functions.hangup(self, caller, code, phrase)
+ require 'common.str'
+
+ if not tonumber(code) then
+ code = 403;
+ phrase = 'Forbidden';
+ end
+
+ if common.str.blank(phrase) then
+ phrase = 'Hangup here';
+ end
+
+ self.log:info("FUNCTION_HANGUP code: ", code, ', phrase: ', phrase);
+ return { continue = false, code = code, phrase = phrase:gsub('_', ' '), no_cdr = true }
+end
diff --git a/misc/freeswitch/scripts/dialplan/geo_number.lua b/misc/freeswitch/scripts/dialplan/geo_number.lua
new file mode 100644
index 0000000..06bfd62
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/geo_number.lua
@@ -0,0 +1,89 @@
+-- Gemeinschaft 5 module: geonumber class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+GeoNumber = {}
+
+-- create phone_book object
+function GeoNumber.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.class = 'geonumber';
+ self.log = arg.log;
+ self.database = arg.database;
+ return object;
+end
+
+function GeoNumber.country(self, phone_number)
+ if phone_number:match('^%+1') then
+ return { id = 0, name = 'NANP', country_code = '1' }
+ end
+
+ local country_codes = {};
+ for i = 2, 4, 1 do
+ table.insert(country_codes, phone_number:sub(2, i));
+ end
+
+ local sql_query = 'SELECT * FROM `countries` WHERE `country_code` IN ("' .. table.concat(country_codes, '","') .. '") ORDER BY LENGTH(`country_code`) DESC LIMIT 1';
+
+ local country = nil;
+ self.database:query(sql_query, function(entry)
+ country = entry;
+ end)
+
+ return country;
+end
+
+
+function GeoNumber.area_code(self, phone_number, country_code)
+ local sql_query = nil;
+ local area_code = nil;
+
+ if country_code == '1' then
+ area_code = {}
+ area_code.area_code, area_code.central_office_code, area_code.subscriber_number, area_code.extension = phone_number:match('%+1(%d%d%d)(%d%d%d)(%d%d%d%d)(%d*)');
+ sql_query = 'SELECT `a`.`name`, `b`.`name` AS `country` FROM `area_codes` `a` \
+ JOIN `countries` `b` ON `a`.`country_id` = `b`.`id` \
+ WHERE `b`.`country_code` = "' .. tostring(country_code) .. '"\
+ AND `a`.`area_code` = "' .. tostring(area_code.area_code) .. '" \
+ AND `a`.`central_office_code` = "' .. tostring(area_code.central_office_code) .. '" LIMIT 1';
+ else
+ local offset = #country_code;
+ area_codes = {};
+ for i = (3 + offset), (6 + offset), 1 do
+ table.insert(area_codes, phone_number:sub((2 + offset), i));
+ end
+
+ sql_query = 'SELECT `a`.`name`, `b`.`name` AS `country` FROM `area_codes` `a` \
+ JOIN `countries` `b` ON `a`.`country_id` = `b`.`id` \
+ WHERE `b`.`country_code` = "' .. country_code .. '"\
+ AND `a`.`area_code` IN ("' .. table.concat(area_codes, '","') .. '") ORDER BY LENGTH(`a`.`area_code`) DESC LIMIT 1';
+ end
+
+ self.database:query(sql_query, function(entry)
+ area_code = entry;
+ end)
+
+ return area_code;
+end
+
+
+function GeoNumber.find(self, phone_number)
+ if not phone_number:match('^%+%d+') then
+ return nil;
+ end
+
+ local country = self:country(phone_number);
+ if country then
+ local area_code = self:area_code(phone_number, country.country_code);
+ if area_code then
+ return area_code;
+ else
+ return { country = country.name };
+ end
+ end
+end
diff --git a/misc/freeswitch/scripts/dialplan/hunt_group.lua b/misc/freeswitch/scripts/dialplan/hunt_group.lua
new file mode 100644
index 0000000..87f86f1
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/hunt_group.lua
@@ -0,0 +1,202 @@
+-- Gemeinschaft 5 module: hunt group class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+HuntGroup = {}
+
+local DEFAULT_MEMBER_TIMEOUT = 20;
+
+-- Create HuntGroup object
+function HuntGroup.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.class = 'huntgroup';
+ self.log = arg.log;
+ self.database = arg.database;
+ self.record = arg.record;
+ return object;
+end
+
+
+function HuntGroup.find_by_id(self, id)
+ local sql_query = 'SELECT * FROM `hunt_groups` WHERE `id`= '.. tonumber(id) .. ' LIMIT 1';
+ local hunt_group = nil;
+
+ self.database:query(sql_query, function(entry)
+ hunt_group = HuntGroup:new(self);
+ hunt_group.record = entry;
+ hunt_group.id = tonumber(entry.id);
+ hunt_group.uuid = entry.uuid;
+ end)
+
+ return hunt_group;
+end
+
+
+function HuntGroup.find_by_uuid(self, uuid)
+ local sql_query = 'SELECT * FROM `hunt_groups` WHERE `id`= "'.. uuid .. '" LIMIT 1';
+ local hunt_group = nil;
+
+ self.database:query(sql_query, function(entry)
+ hunt_group = HuntGroup:new(self);
+ hunt_group.record = entry;
+ hunt_group.id = tonumber(entry.id);
+ hunt_group.uuid = entry.uuid;
+ end)
+
+ return hunt_group;
+end
+
+
+function HuntGroup.list_active_members(self)
+ local sql_query = 'SELECT `a`.`number`, `b`.`name` \
+ FROM `phone_numbers` `a` \
+ LEFT JOIN `hunt_group_members` `b` ON `a`.`phone_numberable_type` = "huntgroupmember" AND `a`.`phone_numberable_id` = `b`.`id` \
+ WHERE `a`.`phone_numberable_type` = "huntgroupmember" \
+ AND `b`.`active` IS TRUE \
+ AND `b`.`hunt_group_id` = ' .. self.record.id;
+
+ local hunt_group_members = {}
+
+ self.database:query(sql_query, function(hunt_group_members_entry)
+ table.insert(hunt_group_members, hunt_group_members_entry);
+ end)
+
+ return hunt_group_members;
+end
+
+
+function HuntGroup.is_member_by_numbers(self, numbers)
+ local sql_query = 'SELECT `a`.`number`, `b`.`name` \
+ FROM `phone_numbers` `a` \
+ LEFT JOIN `hunt_group_members` `b` ON `a`.`phone_numberable_type` = "huntgroupmember" AND `a`.`phone_numberable_id` = `b`.`id` \
+ WHERE `a`.`phone_numberable_type` = "huntgroupmember" \
+ AND `b`.`active` IS TRUE \
+ AND `b`.`hunt_group_id` = ' .. self.record.id .. '\
+ AND `a`.`number` IN ("' .. table.concat( numbers, '","') .. '") LIMIT 1';
+
+ local hunt_group_member = false;
+
+ self.database:query(sql_query, function(hunt_group_members_entry)
+ hunt_group_member = true;
+ end)
+
+ return hunt_group_member;
+end
+
+
+function HuntGroup.run(self, dialplan_object, caller, destination)
+ local hunt_group_members = self:list_active_members();
+
+ if #hunt_group_members == 0 then
+ return { disposition = 'HUNT_GROUP_EMPTY', code = 480, phrase = 'No active users' }
+ end
+
+ self.log:info('HUNTGROUP ', self.record.id, ' - name: ', self.record.name, ', strategy: ', self.record.strategy,', members: ', #hunt_group_members);
+
+ local destinations = {}
+ for index, hunt_group_member in ipairs(hunt_group_members) do
+ local destination = dialplan_object:destination_new{ number = hunt_group_member.number };
+ if destination.type == 'unknown' then
+ require 'dialplan.route'
+ local routes = dialplan.route.Route:new{ log = self.log, database = self.database, routing_table = dialplan_object.routes }:outbound(caller, destination.number);
+ if routes and #routes > 0 then
+ destination.callee_id_number = destination.number;
+ destination.callee_id_name = nil;
+ local route = routes[1];
+ destination.gateway = route.endpoint;
+ destination.type = route.class;
+ destination.number = route.value;
+ destination.caller_id_number = route.caller_id_number;
+ destination.caller_id_name = route.caller_id_name;
+ table.insert(destinations, destination);
+ end
+ else
+ table.insert(destinations, destination);
+ end
+ end
+
+ local forwarding_destination = nil;
+ if caller.forwarding_service == 'assistant' and caller.auth_account then
+ forwarding_destination = dialplan_object:destination_new{ type = caller.auth_account.class, id = caller.auth_account.id, number = forwarding_number }
+ forwarding_destination.alert_info = 'http://amooma.com;info=Ringer0;x-line-id=0';
+ end
+
+ local result = { continue = false };
+ local start_time = os.time();
+ if self.record.strategy == 'ring_recursively' then
+ local member_timeout = tonumber(self.record.seconds_between_jumps) or DEFAULT_MEMBER_TIMEOUT;
+ local run_queue = true;
+ while run_queue do
+ for index, member_destination in ipairs(destinations) do
+ local recursive_destinations = { member_destination }
+ if forwarding_destination then
+ table.insert(recursive_destinations, forwarding_destination);
+ end
+ require 'dialplan.sip_call'
+ result = dialplan.sip_call.SipCall:new{ log = self.log, database = self.database, caller = caller }:fork( recursive_destinations, { callee_id_number = destination.number, timeout = member_timeout });
+ if result.disposition == 'SUCCESS' then
+ if result.fork_index then
+ result.destination = recursive_destinations[result.fork_index];
+ end
+ run_queue = false;
+ break;
+ elseif os.time() > start_time + dialplan_object.dial_timeout_active then
+ run_queue = false;
+ break;
+ elseif not caller:ready() then
+ run_queue = false;
+ break;
+ end
+ end
+ if tostring(result.code) == '486' then
+ self.log:info('HUNTGROUP ', self.record.id, ' - all members busy');
+ run_queue = false;
+ end
+ end
+ else
+ if forwarding_destination then
+ table.insert(destinations, forwarding_destination);
+ end
+
+ require 'dialplan.sip_call'
+ result = dialplan.sip_call.SipCall:new{ log = self.log, database = self.database, caller = caller }:fork( destinations,
+ {
+ callee_id_number = destination.number,
+ timeout = dialplan_object.dial_timeout_active,
+ send_ringing = ( dialplan_object.send_ringing_to_gateways and caller.from_gateway ),
+ });
+ if result.fork_index then
+ result.destination = destinations[result.fork_index];
+ end
+
+ return result;
+ end
+
+ return result;
+end
+
+
+function HuntGroup.list_destination_numbers(self)
+ require "common.phone_number"
+ local phone_number_class = common.phone_number.PhoneNumber:new(defaults)
+
+ local sql_query = string.format("SELECT * FROM `phone_numbers` WHERE `state`='active' AND `phone_numberable_type` = 'HuntGroupMember' AND `phone_numberable_id` IN ( \
+ SELECT `id` FROM `hunt_group_members` WHERE `active` IS TRUE AND `hunt_group_id`=%d ) ORDER BY `position` ASC", tonumber(self.record.id));
+ local phone_numbers = {}
+
+ self.database:query(sql_query, function(hunt_group_number_entry)
+ local number_object = phone_number_class:find_by_number(hunt_group_number_entry.number)
+ if number_object and number_object.record then
+ table.insert(phone_numbers, {number = hunt_group_number_entry.number, destination_type = number_object.record.phone_numberable_type, destination_id = number_object.record.phone_numberable_id});
+ else
+ table.insert(phone_numbers, {number = hunt_group_number_entry.number});
+ end
+ end)
+
+ return phone_numbers ;
+end
diff --git a/misc/freeswitch/scripts/dialplan/phone_book.lua b/misc/freeswitch/scripts/dialplan/phone_book.lua
new file mode 100644
index 0000000..089f115
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/phone_book.lua
@@ -0,0 +1,63 @@
+-- Gemeinschaft 5 module: phone book class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+PhoneBook = {}
+
+-- create phone_book object
+function PhoneBook.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.class = 'phonebook';
+ self.log = arg.log;
+ self.database = arg.database;
+ return object;
+end
+
+
+function PhoneBook.find_entry_by_number_user_tenant(self, numbers, user_id, tenant_id)
+ user_id = tonumber(user_id) or 0;
+ tenant_id = tonumber(tenant_id) or 0;
+
+ if not numbers or #numbers == 0 then
+ return nil;
+ end
+ local numbers_sql = '"' .. table.concat(numbers, '","') .. '"';
+
+ local sql_query = 'SELECT `a`.`name` AS `number_name`, \
+ `a`.`number`, \
+ `b`.`id`, \
+ `b`.`value_of_to_s`, \
+ `b`.`phone_book_id`, \
+ `b`.`image`, \
+ `c`.`name` AS `phone_book_name`, \
+ `d`.`bellcore_id` \
+ FROM `phone_numbers` `a` \
+ JOIN `phone_book_entries` `b` ON `a`.`phone_numberable_id` = `b`.`id` AND `a`.`phone_numberable_type` = "PhoneBookENtry" \
+ JOIN `phone_books` `c` ON `b`.`phone_book_id` = `c`.`id` \
+ LEFT JOIN `ringtones` `d` ON `a`.`id` = `d`.`ringtoneable_id` AND `d`.`ringtoneable_type` = "PhoneNumber" \
+ WHERE ((`c`.`phone_bookable_type` = "User" AND `c`.`phone_bookable_id` = ' .. user_id .. ') \
+ OR (`c`.`phone_bookable_type` = "Tenant" AND `c`.`phone_bookable_id` = ' .. tenant_id .. ')) \
+ AND `a`.`number` IN (' .. numbers_sql .. ') \
+ AND `a`.`state` = "active" \
+ AND `b`.`state` = "active" \
+ AND `c`.`state` = "active" \
+ ORDER BY `c`.`phone_bookable_type` DESC LIMIT 1';
+
+ local phone_book_entry = nil;
+
+ self.database:query(sql_query, function(entry)
+ phone_book_entry = entry;
+ if entry.number_name then
+ phone_book_entry.caller_id_name = tostring(entry.value_of_to_s) .. ' (' .. entry.number_name .. ')';
+ else
+ phone_book_entry.caller_id_name = entry.value_of_to_s;
+ end
+ end)
+
+ return phone_book_entry;
+end
diff --git a/misc/freeswitch/scripts/dialplan/presence.lua b/misc/freeswitch/scripts/dialplan/presence.lua
new file mode 100644
index 0000000..234b908
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/presence.lua
@@ -0,0 +1,84 @@
+-- Gemeinschaft 5 module: presence class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+Presence = {}
+
+-- Create Presence object
+function Presence.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self)
+ self.__index = self
+ self.log = arg.log;
+ self.domain = arg.domain;
+ self.uuid = arg.uuid;
+ self.inbound = arg.inbound;
+ self.accounts = arg.accounts;
+
+ return object
+end
+
+
+function Presence.init(self, arg)
+ self.log = arg.log or self.log;
+ self.domain = arg.domain or self.domain;
+ self.uuid = arg.uuid or self.uuid;
+ self.inbound = arg.inbound or self.inbound;
+ self.accounts = arg.accounts or self.accounts;
+end
+
+
+function Presence.set(self, state, caller_number)
+ if not self.accounts or #self.accounts == 0 then
+ return nil;
+ end
+
+ state = state or "terminated";
+ local direction = "outbound";
+
+ if self.inbound then
+ direction = "inbound";
+ end
+
+ for index, account in pairs(self.accounts) do
+ if account ~= '' then
+ local event = freeswitch.Event('PRESENCE_IN');
+ event:addHeader('proto', 'sip');
+ event:addHeader('from', account .. '@' .. self.domain);
+ event:addHeader('event_type', 'presence');
+ event:addHeader('alt_event_type', 'dialog');
+ event:addHeader('presence-call-direction', direction);
+ event:addHeader('answer-state', state);
+ event:addHeader('unique-id', self.uuid);
+ if caller_number then
+ if self.inbound then
+ event:addHeader('Caller-Destination-Number', caller_number);
+ else
+ event:addHeader('Other-Leg-Caller-ID-Number', caller_number);
+ end
+ end
+ event:fire();
+ self.log:debug('PRESENCE - account: ' .. account .. '@' .. self.domain .. ', state: ' .. state .. ', direction: ' .. direction .. ', uid: ' ..self.uuid);
+ end
+ end
+
+ return true;
+end
+
+
+function Presence.early(self, caller_number)
+ return self:set("early", caller_number);
+end
+
+
+function Presence.confirmed(self, caller_number)
+ return self:set("confirmed", caller_number);
+end
+
+
+function Presence.terminated(self, caller_number)
+ return self:set("terminated", caller_number);
+end
diff --git a/misc/freeswitch/scripts/dialplan/route.lua b/misc/freeswitch/scripts/dialplan/route.lua
new file mode 100644
index 0000000..2243cbe
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/route.lua
@@ -0,0 +1,265 @@
+-- Gemeinschaft 5 module: routing class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+Route = {}
+
+-- create route object
+function Route.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.log = arg.log;
+ self.database = arg.database;
+ self.routing_table = arg.routing_table;
+ self.expandable = arg.expandable or {};
+ return object;
+end
+
+-- find matching routes
+function Route.prerouting(self, caller, number)
+ require 'common.routing_tables'
+
+ for index, routing_entry in pairs(self.routing_table.prerouting) do
+ local route = common.routing_tables.match_route(routing_entry, number);
+ if route.error then
+ self.log:error('PREROUTE - error: ', route.error);
+ elseif route.value then
+ self.log:info('ROUTE_PREROUTING - called number: ', number, ', value: ', route.value, ', pattern: ', route.pattern);
+ return route;
+ end
+ end
+end
+
+-- find matching routes
+function Route.outbound(self, caller, number)
+ local routes = {};
+ require 'common.routing_tables'
+ require 'common.str'
+
+ local ignore_arguments = {
+ class=true,
+ endpoint=true,
+ pattern=true,
+ value=true,
+ group=true,
+ phrase=true,
+ }
+
+ local clip_no_screening = common.str.try(caller, 'account.record.clip_no_screening');
+ local caller_id_numbers = {}
+ if not common.str.blank(clip_no_screening) then
+ for index, number in ipairs(common.str.strip_to_a(clip_no_screening, ',')) do
+ table.insert(caller_id_numbers, number);
+ end
+ end
+ for index, number in ipairs(caller.caller_phone_numbers) do
+ table.insert(caller_id_numbers, number);
+ end
+ self.log:info('CALLER_ID_NUMBER - caller_id_numbers: ', table.concat(caller_id_numbers, ','));
+
+ for index, routing_entry in pairs(self.routing_table.outbound) do
+ local route = common.routing_tables.match_route(routing_entry, number);
+ if route.error then
+ self.log:error('ROUTE_OUTBOUND - error: ', route.error);
+ elseif route.value then
+ local valid_route = true;
+
+ for argument, value in pairs(route) do
+ if not ignore_arguments[argument] then
+ local table_value = common.str.downcase(tostring(common.str.try(caller, argument)));
+ value = common.str.downcase(tostring(value));
+ if table_value:match(value) then
+ self.log:info('ROUTE_OUTBOUND_POSITIVE - ', argument, '=', value, ' ~ ', table_value, ', pattern: ', route.pattern);
+ else
+ self.log:info('ROUTE_OUTBOUND_NEGATIVE - ', argument, '=', value, ' !~ ', table_value, ', pattern: ', route.pattern);
+ valid_route = false;
+ end
+ end
+ end
+
+ if route.group then
+ if common.str.try(caller.auth_account, 'owner.groups.' .. tostring(route.group)) then
+ self.log:info('ROUTE_OUTBOUND_POSITIVE - group=', route.group, ', pattern: ', route.pattern);
+ else
+ self.log:info('ROUTE_OUTBOUND_NEGATIVE - group=', route.group, ', pattern: ', route.pattern);
+ valid_route = false;
+ end
+ end
+
+ if route.cidn then
+ if caller.caller_id_number:match(route.cidn) then
+ self.log:info('ROUTE_OUTBOUND_POSITIVE - cidn=', route.cidn, ' ~ ', caller.caller_id_number,', pattern: ', route.pattern);
+ else
+ self.log:info('ROUTE_OUTBOUND_NEGATIVE - cidn=', route.cidn, ' !~ ', caller.caller_id_number, ', pattern: ', route.pattern);
+ valid_route = false;
+ end
+ end
+
+ if valid_route then
+ if route.class ~= 'hangup' then
+ route.caller_id_number = self:outbound_cid_number(caller, caller_id_numbers, route.endpoint, route.class);
+ self.expandable.caller_id_number = route.caller_id_number;
+ route.caller_id_name = self:outbound_cid_name(caller, route.endpoint, route.class);
+ end
+ table.insert(routes, route);
+ self.log:info('ROUTE_OUTBOUND ', #routes,' - ', route.class, '=', route.endpoint, ', value: ', route.value, ', caller_id_number: ', route.caller_id_number, ', caller_id_name: ', route.caller_id_name);
+ end
+ end
+ end
+
+ return routes;
+end
+
+
+function Route.inbound(self, caller, number)
+ require 'common.routing_tables'
+
+ local ignore_arguments = {
+ class=true,
+ endpoint=true,
+ pattern=true,
+ value=true,
+ group=true,
+ phrase=true,
+ }
+
+ for index, routing_entry in pairs(self.routing_table.inbound) do
+ local route = common.routing_tables.match_route(routing_entry, number);
+ if route.error then
+ self.log:error('ROUTE_INBOUND - error: ', route.error);
+ elseif route.value then
+ local valid_route = true;
+
+ for argument, value in pairs(route) do
+ if not ignore_arguments[argument] then
+ local table_value = common.str.downcase(tostring(common.str.try(caller, argument)));
+ value = common.str.downcase(tostring(value));
+ if table_value:match(value) then
+ self.log:info('ROUTE_INBOUND_POSITIVE - ', argument, '=', value, ' ~ ', table_value, ', pattern: ', route.pattern);
+ else
+ self.log:info('ROUTE_INBOUND_NEGATIVE - ', argument, '=', value, ' !~ ', table_value, ', pattern: ', route.pattern);
+ valid_route = false;
+ end
+ end
+ end
+
+ if route.class and route.endpoint then
+ if route.class == 'gateway' and caller.gateway_name:match(route.endpoint) then
+ self.log:info('ROUTE_INBOUND_POSITIVE - ', route.class, '=', route.endpoint, ' ~ ', caller.gateway_name, ', pattern: ', route.pattern);
+ else
+ self.log:info('ROUTE_INBOUND_NEGATIVE - ', route.class, '=', route.endpoint, ' !~ ', caller.gateway_name, ', pattern: ', route.pattern);
+ valid_route = false;
+ end
+ end
+
+ if valid_route then
+ self.log:info('ROUTE_INBOUND - called number: ', number, ', value: ', route.value, ', pattern: ', route.pattern);
+ return route;
+ end
+ end
+ end
+end
+
+-- find caller id
+function Route.caller_id(self, caller, cid_entry, search_str, endpoint, class)
+ local ignore_arguments = {
+ class=true,
+ endpoint=true,
+ pattern=true,
+ value=true,
+ group=true,
+ phrase=true,
+ }
+
+ local route = common.routing_tables.match_route(cid_entry, search_str, self.expandable);
+ if route.error then
+ self.log:error('CALLER_ID - error: ', route.error);
+ elseif route.value then
+ local valid_route = true;
+
+ for argument, value in pairs(route) do
+ if not ignore_arguments[argument] then
+ local table_value = common.str.downcase(tostring(common.str.try(caller, argument)));
+ value = common.str.downcase(tostring(value));
+ if table_value:match(value) then
+ self.log:debug('CALLER_ID_POSITIVE - ', argument, '=', value, ' ~ ', table_value, ', pattern: ', route.pattern);
+ else
+ self.log:debug('CALLER_ID_NEGATIVE - ', argument, '=', value, ' !~ ', table_value, ', pattern: ', route.pattern);
+ valid_route = false;
+ end
+ end
+ end
+
+ if route.group then
+ if common.str.try(caller.auth_account, 'owner.groups.' .. tostring(route.group)) then
+ self.log:debug('CALLER_ID_POSITIVE - group=', route.group, ', pattern: ', route.pattern);
+ else
+ self.log:debug('CALLER_ID_NEGATIVE - group=', route.group, ', pattern: ', route.pattern);
+ valid_route = false;
+ end
+ end
+
+ endpoint = tostring(endpoint);
+ if route.class and route.endpoint then
+ if route.class == 'gateway' and endpoint:match(route.endpoint) then
+ self.log:debug('CALLER_ID_POSITIVE - ', route.class, '=', route.endpoint, ' ~ ', endpoint, ', pattern: ', route.pattern);
+ else
+ self.log:debug('CALLER_ID_NEGATIVE - ', route.class, '=', route.endpoint, ' !~ ', endpoint, ', pattern: ', route.pattern);
+ valid_route = false;
+ end
+ end
+
+ if valid_route then
+ self.log:debug('CALLER_ID ', route.class, '=', route.endpoint, ', value: ', route.value);
+ return route.value;
+ end
+ end
+
+ return nil;
+end
+
+-- find matching caller id number
+function Route.outbound_cid_number(self, caller, caller_id_numbers, endpoint, class)
+ for route_index, cid_entry in pairs(self.routing_table.outbound_cid_number) do
+ for index, number in ipairs(caller_id_numbers) do
+ local route = self:caller_id(caller, cid_entry, number, endpoint, class);
+ if route then
+ return route;
+ end
+ end
+ end
+end
+
+-- find matching caller id name
+function Route.outbound_cid_name(self, caller, endpoint, class)
+ for route_index, cid_entry in pairs(self.routing_table.outbound_cid_name) do
+ local route = self:caller_id(caller, cid_entry, caller.caller_id_name, endpoint, class);
+ if route then
+ return route;
+ end
+ end
+end
+
+-- find matching caller id number
+function Route.inbound_cid_number(self, caller, endpoint, class)
+ for route_index, cid_entry in pairs(self.routing_table.inbound_cid_number) do
+ local route = self:caller_id(caller, cid_entry, caller.caller_id_number, endpoint, class);
+ if route then
+ return route;
+ end
+ end
+end
+
+-- find matching caller id name
+function Route.inbound_cid_name(self, caller, endpoint, class)
+ for route_index, cid_entry in pairs(self.routing_table.inbound_cid_name) do
+ local route = self:caller_id(caller, cid_entry, caller.caller_id_name, endpoint, class);
+ if route then
+ return route;
+ end
+ end
+end
diff --git a/misc/freeswitch/scripts/dialplan/session.lua b/misc/freeswitch/scripts/dialplan/session.lua
new file mode 100644
index 0000000..7174b24
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/session.lua
@@ -0,0 +1,224 @@
+-- Gemeinschaft 5 module: caller session class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+Session = {}
+
+-- create session object
+function Session.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.log = arg.log;
+ self.session = arg.session;
+
+ if not self.session then
+ return nil;
+ end
+
+ return object;
+end
+
+function Session.init_channel_variables(self)
+ self.cause = "UNSPECIFIED"
+
+ self.uuid = self.session:get_uuid();
+ self.destination_number = self:expand_variables(self:to_s('destination_number'));
+ self.called_number = self.destination_number;
+
+ self.caller_id_number = self:to_s('caller_id_number');
+ self.caller_id_name = self:to_s('caller_id_name');
+ self.caller_phone_number = self.caller_id_number;
+ self.caller_phone_numbers = {self.caller_id_number};
+
+ self.domain = self:to_s('domain_name');
+ self.gateway_name = self:to_s('sip_gateway');
+ self.from_gateway = self:to_b('gs_from_gateway');
+ if self.from_gateway then
+ self.gateway_name = self:to_s('gs_gateway_name');
+ elseif self.gateway_name ~= '' then
+ self.from_gateway = true;
+ end
+
+ self.account_uuid = self:to_s('gs_account_uuid');
+ self.account_type = self:to_s('gs_account_type');
+ self.sip_contact_host = self:to_s('sip_contact_host');
+ self.clir = self:to_b('gs_clir');
+ self.call_timeout = self:to_i('gs_call_timeout');
+ self.auth_account_type = self:to_s('gs_auth_account_type');
+ self.auth_account_uuid = self:to_s('gs_auth_account_uuid');
+
+ self.node_id = self:to_i('sip_h_X-GS_node_id');
+ self.loop_count = self:to_i('sip_h_X-GS_loop_count');
+
+ if self.node_id > 0 and self.node_id ~= self.local_node_id then
+ self.from_node = true;
+ else
+ self.from_node = false;
+ end
+ self:set_variable('gs_account_node_local', not self.from_node);
+
+ if self.from_node then
+ self.account_uuid = self:to_s('sip_h_X-GS_account_uuid');
+ self.account_type = self:to_s('sip_h_X-GS_account_type');
+ self.auth_account_uuid = self:to_s('sip_h_X-GS_auth_account_uuid');
+ self.auth_account_type = self:to_s('sip_h_X-GS_auth_account_type');
+ end
+
+ if self.auth_account_type == '' then
+ self.auth_account_type = self.account_type;
+ self.auth_account_uuid = self.account_uuid;
+ end
+
+ self.forwarding_number = nil;
+ self.forwarding_service = nil;
+
+ return true;
+end
+
+
+-- Cast channel variable to string
+function Session.to_s(self, variable_name)
+ require 'common.str'
+ return common.str.to_s(self.session:getVariable(variable_name));
+end
+
+-- Cast channel variable to integer
+function Session.to_i(self, variable_name)
+ require 'common.str'
+ return common.str.to_i(self.session:getVariable(variable_name));
+end
+
+-- Cast channel variable to boolean
+function Session.to_b(self, variable_name)
+ require 'common.str'
+ return common.str.to_b(self.session:getVariable(variable_name));
+end
+
+-- Split channel variable to table
+function Session.to_a(self, variable_name)
+ require 'common.str'
+ return common.str.to_a(self.session:getVariable(variable_name));
+end
+
+-- Check if session is active
+function Session.ready(self, command, parameters)
+ return self.session:ready();
+end
+
+-- Wait milliseconds
+function Session.sleep(self, milliseconds)
+ return self.session:sleep(milliseconds);
+end
+
+-- Execute command
+function Session.execute(self, command, parameters)
+ parameters = parameters or '';
+ self.session:execute(command, parameters);
+end
+
+-- Execute and return result
+function Session.result(self, command_line)
+ self.session:execute('set', 'result=${' .. command_line .. '}');
+ return self.session:getVariable('result');
+end
+
+-- Set cause code
+function Session.set_cause(self, cause)
+ self.cause = cause
+end
+
+-- Set channel variable
+function Session.set_variable(self, name, value)
+ self.session:setVariable(name, tostring(value));
+end
+
+-- Set and export channel variable
+function Session.export_variable(self, name, value)
+ self.session:execute('export', tostring(name) .. '=' .. tostring(value));
+end
+
+-- Set SIP header
+function Session.set_header(self, name, value)
+ self.session:setVariable('sip_h_' .. name, tostring(value));
+end
+
+-- Hangup a call
+function Session.hangup(self, cause)
+ return self.session:hangup(cause);
+end
+
+-- Respond a call
+function Session.respond(self, code, text)
+ self.session:execute('respond', tostring(code) .. ' ' .. text);
+ return self.session:hangupCause();
+end
+
+-- Answer a call
+function Session.answer(self)
+ return self.session:answer();
+end
+
+function Session.intercept(self, uid)
+ self.session:execute("intercept", uid);
+end
+
+function Session.send_display(self, ... )
+ self:execute('send_display', table.concat( arg, '|'));
+end
+
+-- Set caller ID
+function Session.set_caller_id(self, number, name)
+ if number then
+ self.caller_id_number = tostring(number);
+ self.session:setVariable('effective_caller_id_number', tostring(number))
+ end
+ if name then
+ self.caller_id_name = tostring(name);
+ self.session:setVariable('effective_caller_id_name', tostring(name))
+ end
+end
+
+-- Set callee ID
+function Session.set_callee_id(self, number, name)
+ if number ~= nil then
+ self.callee_id_number = tostring(number);
+ self.session:execute('export', 'effective_callee_id_number=' .. number);
+ end
+ if name ~= nil then
+ self.callee_id_name = tostring(name);
+ self.session:execute('export', 'effective_callee_id_name=' .. name);
+ end
+end
+
+-- Set caller Privacy header
+function Session.set_privacy(self, privacy)
+ if privacy then
+ self.session:setVariable('cid_type', 'none');
+ self.session:setVariable('sip_h_Privacy', 'id');
+ else
+ self.session:setVariable('cid_type', 'none');
+ self.session:setVariable('sip_h_Privacy', 'none');
+ end
+end
+
+
+function Session.set_auth_account(self, auth_account)
+ if auth_account then
+ self:set_variable('gs_auth_account_type', auth_account.class);
+ self:set_variable('gs_auth_account_id', auth_account.id);
+ self:set_variable('gs_auth_account_uuid', auth_account.uuid);
+ end
+
+ return auth_account;
+end
+
+
+function Session.expand_variables(self, line)
+ return (line:gsub('{([%a%d_-]+)}', function(captured)
+ return self.session:getVariable(captured) or '';
+ end))
+end
diff --git a/misc/freeswitch/scripts/dialplan/sip_call.lua b/misc/freeswitch/scripts/dialplan/sip_call.lua
new file mode 100644
index 0000000..57f92c6
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/sip_call.lua
@@ -0,0 +1,266 @@
+-- Gemeinschaft 5 module: sip call class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall);
+
+SipCall = {}
+
+-- Create SipCall object
+function SipCall.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.log = arg.log;
+ self.session = arg.session;
+ self.record = arg.record;
+ self.database = arg.database;
+ self.domain = arg.domain;
+ self.caller = arg.caller;
+ self.on_answer = arg.on_answer;
+ self.calling_object = arg.calling_object;
+ return object;
+end
+
+
+function SipCall.wait_answer(self, caller_session, callee_session, timeout, start_time)
+ if caller_session:ready() and callee_session:ready() then
+ callee_session:waitForAnswer(caller_session);
+ end
+
+ while true do
+ if not caller_session:ready() then
+ return 'ORIGINATOR_CANCEL';
+ elseif not callee_session:ready() then
+ return 'UNSPECIFIED';
+ elseif (os.time() - start_time) > timeout then
+ return 'NO_ANSWER';
+ elseif callee_session:answered() then
+ return 'SUCCESS';
+ end
+
+ self.caller:sleep(500);
+ end
+end
+
+
+function SipCall.wait_hangup(self, caller_session, callee_session)
+ local hangup_on = {
+ CS_HANGUP = true,
+ CS_DESTROY = true,
+ }
+
+ while true do
+ local state_caller = caller_session:getState();
+ local state_callee = callee_session:getState();
+ if hangup_on[state_caller] or hangup_on[state_callee] then
+ break;
+ end
+ caller_session:sleep(500);
+ end
+end
+
+
+function SipCall.call_waiting_busy(self, sip_account)
+ require 'common.str'
+ if common.str.to_b(sip_account.record.call_waiting) then
+ self.log:info('CALL_WAITING - status: enabled');
+ return false;
+ else
+ local state = sip_account:call_state();
+ self.log:info('CALL_WAITING - status: disabled, sip_account state: ', state);
+ return state;
+ end
+end
+
+
+function SipCall.fork(self, destinations, arg )
+ local dial_strings = {}
+
+ require 'common.sip_account'
+ local sip_account_class = common.sip_account.SipAccount:new{ log = self.log, database = self.database };
+
+ local call_result = { code = 404, phrase = 'No destination' };
+ local some_destinations_busy = false;
+
+ for index, destination in ipairs(destinations) do
+ local origination_variables = { 'gs_fork_index=' .. index }
+
+ self.log:info('FORK ', index, '/', #destinations, ' - ', destination.type, '=', destination.id, '/', destination.gateway or destination.uuid, '@', destination.node_id, ', number: ', destination.number);
+ if not destination.node_local or destination.type == 'node' then
+ require 'common.node'
+ local node = nil;
+ if tonumber(destination.gateway) then
+ node = common.node.Node:new{ log = self.log, database = self.database }:find_by_id(tonumber(destination.gateway));
+ else
+ node = common.node.Node:new{ log = self.log, database = self.database }:find_by_id(destination.node_id);
+ end
+ if node then
+ table.insert(origination_variables, 'sip_h_X-GS_node_id=' .. self.caller.local_node_id);
+ table.insert(dial_strings, '[' .. table.concat(origination_variables , ',') .. ']sofia/gateway/' .. node.record.name .. '/' .. destination.number);
+ end
+ elseif destination.type == 'sipaccount' then
+ local callee_id_params = '';
+ local sip_account = sip_account_class:find_by_id(destination.id);
+ local call_waiting = self:call_waiting_busy(sip_account);
+ if not call_waiting then
+ destinations[index].numbers = sip_account:phone_numbers();
+
+ if not arg.callee_id_name then
+ table.insert(origination_variables, "effective_callee_id_name='" .. sip_account.record.caller_name .. "'");
+ end
+ if not arg.callee_id_number then
+ table.insert(origination_variables, "effective_callee_id_number='" .. destination.number .. "'");
+ end
+ if destination.alert_info then
+ table.insert(origination_variables, "alert_info='" .. destination.alert_info .. "'");
+ end
+ table.insert(dial_strings, '[' .. table.concat(origination_variables , ',') .. ']user/' .. sip_account.record.auth_name);
+ else
+ some_destinations_busy = true;
+ call_result = { code = 486, phrase = 'User busy', disposition = 'USER_BUSY' };
+ end
+ elseif destination.type == 'gateway' then
+ if destination.caller_id_number then
+ table.insert(origination_variables, "origination_caller_id_number='" .. destination.caller_id_number .. "'");
+ end
+ if destination.caller_id_name then
+ table.insert(origination_variables, "origination_caller_id_name='" .. destination.caller_id_name .. "'");
+ end
+ table.insert(dial_strings, '[' .. table.concat(origination_variables , ',') .. ']sofia/gateway/' .. destination.gateway .. '/' .. destination.number);
+ elseif destination.type == 'dial' then
+ if destination.caller_id_number then
+ table.insert(origination_variables, "origination_caller_id_number='" .. destination.caller_id_number .. "'");
+ end
+ if destination.caller_id_name then
+ table.insert(origination_variables, "origination_caller_id_name='" .. destination.caller_id_name .. "'");
+ end
+ table.insert(dial_strings, '[' .. table.concat(origination_variables , ',') .. ']' .. destination.number);
+ else
+ self.log:info('FORK ', index, '/', #destinations, ' - unhandled destination type: ', destination.type, ', number: ', destination.number);
+ end
+ end
+
+ if #dial_strings == 0 then
+ self.log:notice('FORK - no active destinations - result: ', call_result.code, ' ', call_result.phrase);
+ return call_result;
+ end
+
+ self.caller:set_callee_id(arg.callee_id_number, arg.callee_id_name);
+ self.caller:set_header('X-GS_account_uuid', self.caller.account_uuid);
+ self.caller:set_header('X-GS_account_type', self.caller.account_type);
+ self.caller:set_header('X-GS_auth_account_type', self.caller.auth_account_type);
+ self.caller:set_header('X-GS_auth_account_uuid', self.caller.auth_account_uuid);
+ self.caller:set_header('X-GS_loop_count', self.caller.loop_count);
+
+ self.caller:set_variable('call_timeout', arg.timeout );
+ self.log:info('FORK DIAL - destinations: ', #dial_strings, ', timeout: ', arg.timeout);
+
+ if arg.send_ringing then
+ self.caller:execute('ring_ready');
+ end
+
+ local start_time = os.time();
+ local session_callee = freeswitch.Session('{local_var_clobber=true}' .. table.concat(dial_strings, ','), self.caller.session);
+ self.log:debug('FORK SESSION_INIT - dial_time: ', os.time() - start_time);
+ local answer_result = self:wait_answer(self.caller.session, session_callee, arg.timeout, start_time);
+ local fork_index = nil;
+ self.log:info('FORK ANSWER - status: ', answer_result, ', dial_time: ', os.time() - start_time);
+ if answer_result == 'SUCCESS' then
+ session_callee:setAutoHangup(false);
+ fork_index = tonumber(session_callee:getVariable('gs_fork_index')) or 0;
+ local destination = destinations[fork_index];
+
+ if arg.bypass_media_network then
+ local callee_uuid = session_callee:get_uuid();
+
+ if callee_uuid and self.caller.uuid and freeswitch then
+ require 'common.ipcalc'
+ local callee_network_str = self.caller:to_s('bleg_network_addr');
+ local caller_network_str = self.caller:to_s('network_addr');
+ local callee_network_addr = common.ipcalc.ipv4_to_i(callee_network_str);
+ local caller_network_addr = common.ipcalc.ipv4_to_i(caller_network_str);
+ local network, netmask = common.ipcalc.ipv4_to_network_netmask(arg.bypass_media_network);
+ if network and netmask and callee_network_addr and caller_network_addr
+ and common.ipcalc.ipv4_in_network(callee_network_addr, network, netmask)
+ and common.ipcalc.ipv4_in_network(caller_network_addr, network, netmask) then
+ self.log:info('FORK ', fork_index, ' BYPASS_MEDIA - caller_ip: ', caller_network_str,
+ ', callee_ip: ', callee_network_str,
+ ', subnet: ', arg.bypass_media_network,
+ ', uuid: ', self.caller.uuid, ', bleg_uuid: ', callee_uuid);
+ freeswitch.API():execute('uuid_media', 'off ' .. self.caller.uuid);
+ freeswitch.API():execute('uuid_media', 'off ' .. callee_uuid);
+ end
+ end
+ end
+
+ if self.on_answer then
+ self.on_answer(self.calling_object, destination);
+ end
+
+ self.caller:set_variable('gs_destination_type', destination.type);
+ self.caller:set_variable('gs_destination_id', destination.id);
+ self.caller:set_variable('gs_destination_uuid', destination.uuid);
+
+ self.log:info('FORK ', fork_index,
+ ' BRIDGE - destination: ', destination.type, '=', destination.id, '/', destination.uuid,'@', destination.node_id,
+ ', number: ', destination.number,
+ ', dial_time: ', os.time() - start_time);
+ freeswitch.bridge(self.caller.session, session_callee);
+ self:wait_hangup(self.caller.session, session_callee);
+ end
+
+ -- if session_callee:ready() then
+ -- self.log:info('FORK - hangup destination channel');
+ -- session_callee:hangup('ORIGINATOR_CANCEL');
+ -- end
+
+ call_result = {};
+ call_result.disposition = session_callee:hangupCause();
+ call_result.fork_index = fork_index;
+
+ if some_destinations_busy and call_result.disposition == 'USER_NOT_REGISTERED' then
+ call_result.phrase = 'User busy';
+ call_result.code = 486;
+ call_result.disposition = 'USER_BUSY';
+ elseif call_result.disposition == 'USER_NOT_REGISTERED' then
+ call_result.phrase = 'User offline';
+ call_result.code = 480;
+ elseif call_result.disposition == 'NO_ANSWER' then
+ call_result.phrase = 'No answer';
+ call_result.code = 408;
+ elseif call_result.disposition == 'NORMAL_TEMPORARY_FAILURE' then
+ call_result.phrase = 'User offline';
+ call_result.code = 480;
+ else
+ call_result.cause = self.caller:to_s('last_bridge_hangup_cause');
+ call_result.code = self.caller:to_i('last_bridge_proto_specific_hangup_cause');
+ call_result.phrase = self.caller:to_s('sip_hangup_phrase');
+ end
+
+ self.log:info('FORK EXIT - disposition: ', call_result.disposition,
+ ', cause: ', call_result.cause,
+ ', code: ', call_result.code,
+ ', phrase: ', call_result.phrase,
+ ', dial_time: ', os.time() - start_time);
+
+ return call_result;
+end
+
+-- Return call forwarding settngs
+function SipCall.conditional_call_forwarding(self, cause, call_forwarding)
+ local condition_map = {USER_NOT_REGISTERED="offline", NO_ANSWER="noanswer", USER_BUSY="busy"}
+ local condition = condition_map[cause]
+ if call_forwarding and condition and call_forwarding[condition] then
+ log:debug('call forwarding on ' .. condition .. ' - destination: ' .. call_forwarding[condition].destination .. ', type: ' .. call_forwarding[condition].call_forwardable_type);
+ return call_forwarding[condition]
+ end
+end
+
+function SipCall.set_callee_variables(self, sip_account)
+ self.session:setVariable("gs_callee_account_id", sip_account.id);
+ self.session:setVariable("gs_callee_account_type", "SipAccount");
+ self.session:setVariable("gs_callee_account_owner_type", sip_account.sip_accountable_type);
+ self.session:setVariable("gs_callee_account_owner_id", sip_account.sip_accountable_id);
+end
diff --git a/misc/freeswitch/scripts/dialplan/tenant.lua b/misc/freeswitch/scripts/dialplan/tenant.lua
new file mode 100644
index 0000000..8d6436c
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/tenant.lua
@@ -0,0 +1,51 @@
+-- Gemeinschaft 5 module: user class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+Tenant = {}
+
+-- Create Tenant object
+function Tenant.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self)
+ self.__index = self;
+ self.class = 'tenant';
+ self.log = arg.log;
+ self.database = arg.database;
+ self.record = arg.record;
+ return object;
+end
+
+-- find tenant by id
+function Tenant.find_by_id(self, id)
+ local sql_query = 'SELECT * FROM `tenants` WHERE `id`= ' .. tonumber(id) .. ' LIMIT 1';
+ local tenant = nil;
+
+ self.database:query(sql_query, function(account_entry)
+ tenant = Tenant:new(self);
+ tenant.record = account_entry;
+ tenant.id = tonumber(account_entry.id);
+ tenant.uuid = account_entry.uuid;
+ end);
+
+ return tenant;
+end
+
+-- find tenant by uuid
+function Tenant.find_by_uuid(self, uuid)
+ tenant_id = tonumber(tenant_id)
+ local sql_query = 'SELECT * FROM `tenants` WHERE `id`= "' .. uuid .. '" LIMIT 1';
+ local tenant = nil;
+
+ self.database:query(sql_query, function(account_entry)
+ tenant = Tenant:new(self);
+ tenant.record = account_entry;
+ tenant.id = tonumber(account_entry.id);
+ tenant.uuid = account_entry.uuid;
+ end);
+
+ return tenant;
+end
diff --git a/misc/freeswitch/scripts/dialplan/user.lua b/misc/freeswitch/scripts/dialplan/user.lua
new file mode 100644
index 0000000..3b483c8
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/user.lua
@@ -0,0 +1,91 @@
+-- Gemeinschaft 5 module: user class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+User = {}
+
+MAX_GROUP_MEMBERSHIPS = 256;
+
+-- create user object
+function User.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.class = 'user';
+ self.log = arg.log;
+ self.database = arg.database;
+ self.record = arg.record;
+ return object;
+end
+
+-- find user by id
+function User.find_by_id(self, id)
+ local sql_query = 'SELECT * FROM `users` WHERE `id`= ' .. tonumber(id) .. ' LIMIT 1';
+ local user = nil;
+
+ self.database:query(sql_query, function(account_entry)
+ user = User:new(self);
+ user.record = account_entry;
+ user.id = tonumber(account_entry.id);
+ user.uuid = account_entry.uuid;
+ end);
+
+ return user;
+end
+
+-- find user by uuid
+function User.find_by_uuid(self, uuid)
+ local sql_query = 'SELECT * FROM `users` WHERE `id`= "' .. uuid .. '" LIMIT 1';
+ local user = nil;
+
+ self.database:query(sql_query, function(account_entry)
+ user = User:new(self);
+ user.record = account_entry;
+ user.id = tonumber(account_entry.id);
+ user.uuid = account_entry.uuid;
+ end);
+
+ return user;
+end
+
+
+function User.list_groups(self, id)
+ require 'common.str'
+ id = id or self.id;
+ local sql_query = 'SELECT `b`.`name` FROM `user_group_memberships` `a` \
+ JOIN `user_groups` `b` ON `a`.`user_group_id` = `b`.`id` \
+ WHERE `a`.`state` = "active" AND `a`.`user_id`= ' .. tonumber(id) .. ' ORDER BY `b`.`position` LIMIT ' .. MAX_GROUP_MEMBERSHIPS;
+
+ local groups = {};
+
+ self.database:query(sql_query, function(entry)
+ groups[common.str.downcase(entry.name)] = true;
+ end);
+
+ return groups;
+end
+
+
+function User.check_pin(self, pin_to_check)
+ if not self.record then
+ return nil
+ end
+
+ local str_to_hash = tostring(self.record.pin_salt) .. tostring(pin_to_check);
+
+ local file = io.popen("echo -n " .. str_to_hash .. "|sha256sum");
+ local pin_to_check_hash = file:read("*a");
+ file:close();
+
+ pin_to_check_hash = pin_to_check_hash:sub(1, 64);
+
+ if pin_to_check_hash == self.record.pin_hash then
+ return true;
+ end
+
+ return false;
+end
+
diff --git a/misc/freeswitch/scripts/dialplan/voicemail.lua b/misc/freeswitch/scripts/dialplan/voicemail.lua
new file mode 100644
index 0000000..b9dab79
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/voicemail.lua
@@ -0,0 +1,155 @@
+-- Gemeinschaft 5 module: voicemail class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+Voicemail = {}
+
+MESSAGE_LENGTH_MIN = 3;
+MESSAGE_LENGTH_MAX = 120;
+SILENCE_LENGTH_ABORT = 5;
+SILENCE_LEVEL = 500;
+BEEP = 'tone_stream://%(1000,0,500)';
+RECORD_FILE_PREFIX = '/tmp/voicemail_';
+
+-- create voicemail object
+function Voicemail.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.class = 'voicemail';
+ self.log = arg.log;
+ self.database = arg.database;
+ self.record = arg.record;
+ return object
+end
+
+-- find voicemail account by sip account id
+function Voicemail.find_by_sip_account_id(self, id)
+ local sql_query = 'SELECT `a`.`id`, `a`.`uuid`, `a`.`auth_name`, `a`.`caller_name`, `b`.`name_path`, `b`.`greeting_path`, `a`.`voicemail_pin`, `b`.`password`, `c`.`host` AS `domain` \
+ FROM `sip_accounts` `a` LEFT JOIN `voicemail_prefs` `b` ON `a`.`auth_name` = `b`.`username` \
+ JOIN `sip_domains` `c` ON `a`.`sip_domain_id` = `c`.`id` \
+ WHERE `a`.`id` = ' .. tonumber(id);
+
+ local voicemail_account = nil;
+ self.database:query(sql_query, function(entry)
+ voicemail_account = Voicemail:new(self);
+ voicemail_account.record = entry;
+ voicemail_account.id = tonumber(entry.id);
+ voicemail_account.uuid = entry.uuid;
+ end)
+
+ return voicemail_account;
+end
+
+-- Find Voicemail account by name
+function Voicemail.find_by_name(self, account_name)
+ id = tonumber(id) or 0;
+ local sql_query = string.format('SELECT * FROM `voicemail_prefs` WHERE `username`= "%s" LIMIT 1', account_name)
+ local record = nil
+
+ self.database:query(sql_query, function(voicemail_entry)
+ record = voicemail_entry
+ end)
+
+ if voicemail_account then
+ voicemail_account.account_name = account_name;
+ if record then
+ voicemail_account.name_path = record.name_path;
+ voicemail_account.greeting_path = record.greeting_path;
+ voicemail_account.password = record.password;
+ end
+ end
+
+ return voicemail_account
+end
+
+-- Find Voicemail account by name
+function Voicemail.find_by_number(self, phone_number)
+ local sip_account = nil;
+
+ require "common.phone_number"
+ local phone_number_class = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database };
+ local destination_number_object = phone_number_class:find_by_number(phone_number);
+ if destination_number_object and destination_number_object.record.phone_numberable_type == "SipAccount" then
+ return Voicemail:find_by_sip_account_id(destination_number_object.record.phone_numberable_id);
+ end
+
+ return false;
+end
+
+
+function Voicemail.leave(self, caller, phone_number)
+ require 'common.str'
+
+ self.log:info('VOICEMAIL_LEAVE - account=', self.record.id, '/', self.record.uuid, ', auth_name: ', self.record.auth_name, ', caller_name: ', self.record.caller_name);
+
+ caller:set_callee_id(phone_number, self.record.caller_name);
+ caller:answer();
+ caller:send_display(common.str.to_s(self.record.caller_name), common.str.to_s(phone_number));
+ caller:sleep(1000);
+
+ if not common.str.blank(self.record.greeting_path) then
+ caller.session:sayPhrase('voicemail_play_greeting', 'greeting:' .. tostring(self.record.greeting_path));
+ elseif not common.str.blank(self.record.name_path) then
+ caller.session:sayPhrase('voicemail_play_greeting', 'name:' .. tostring(self.record.name_path));
+ elseif not common.str.blank(phone_number) then
+ caller.session:sayPhrase('voicemail_play_greeting', (tostring(phone_number):gsub('[%D]', '')));
+ end
+
+ local record_file_name = RECORD_FILE_PREFIX .. caller.uuid .. '.wav';
+ caller.session:streamFile(BEEP);
+ self.log:info('VOICEMAIL_LEAVE - recording to file: ', tostring(record_file_name));
+ local result = caller.session:recordFile(record_file_name, MESSAGE_LENGTH_MAX, SILENCE_LEVEL, SILENCE_LENGTH_ABORT);
+ local duration = caller:to_i('record_seconds');
+
+ if duration >= MESSAGE_LENGTH_MIN then
+ self.log:info('VOICEMAIL_LEAVE - saving recorded message to voicemail, duration: ', duration);
+ require 'common.fapi'
+ common.fapi.FApi:new{ log = self.log, uuid = caller.uuid }:execute('vm_inject',
+ self.record.auth_name ..
+ '@' .. self.record.domain .. " '" ..
+ record_file_name .. "' '" ..
+ caller.caller_id_number .. "' '" ..
+ caller.caller_id_name .. "' '" ..
+ caller.uuid .. "'"
+ );
+ caller:set_variable('voicemail_message_len', duration);
+ else
+ caller:set_variable('voicemail_message_len');
+ end
+ os.remove(record_file_name);
+ return true;
+end
+
+
+function Voicemail.send_notify(self, caller)
+ self.log:debug('VOICEMAIL_NOTIFY - account: ' .. self.record.auth_name .. ", id: " .. tostring(caller.uuid));
+
+ local file = io.popen("/opt/GS5/script/voicemail_new.sh '" .. tostring(self.record.auth_name) .. "' '" .. tostring(caller.uuid) .. "' 2>&1");
+ self.log:debug('VOICEMAIL_NOTIFY - result: ' .. tostring(file:read("*a")));
+ file:close();
+
+ return true;
+end
+
+
+function Voicemail.menu(self, caller, authorized)
+ self.log:info('VOICEMAIL_MENU - account: ', self.record.auth_name);
+
+ if authorized then
+ caller:set_variable('voicemail_authorized', true);
+ end
+
+ caller:set_callee_id(phone_number, self.record.caller_name);
+ caller:answer();
+ caller:send_display(common.str.to_s(self.record.caller_name), common.str.to_s(phone_number));
+
+ caller:sleep(1000);
+ caller:set_variable('skip_greeting', true);
+ caller:set_variable('skip_instructions', true);
+
+ caller:execute('voicemail', 'check default ' .. self.record.domain .. ' ' .. self.record.auth_name);
+end
diff --git a/misc/freeswitch/scripts/dialplan_default.lua b/misc/freeswitch/scripts/dialplan_default.lua
new file mode 100644
index 0000000..ee4a88f
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan_default.lua
@@ -0,0 +1,64 @@
+-- Gemeinschaft 5 default dialplan
+-- (c) AMOOMA GmbH 2012
+--
+
+
+function hangup_hook_caller(s, status, arg)
+ log:info('HANGUP_HOOK: ', status)
+ if tostring(status) == 'transfer' then
+ if start_caller and start_caller.destination then
+ log:info('CALL_TRANSFERRED - destination was: ', start_caller.destination.type, '=', start_caller.destination.id,', number: ' .. tostring(start_caller.destination.number) .. ', to: ' .. start_caller:to_s('sip_refer_to'));
+ start_caller.auth_account = start_dialplan:object_find(start_caller.destination.type, start_caller.destination.id);
+ start_caller.forwarding_number = start_caller.destination.number;
+ start_caller.forwarding_service = 'transfer';
+ end
+ end
+end
+
+-- initialize logging
+require 'common.log'
+log = common.log.Log:new{ prefix = '### [' .. session:get_uuid() .. '] ' };
+
+-- caller session object
+require 'dialplan.session'
+start_caller = dialplan.session.Session:new{ log = log, session = session };
+
+-- dialplan object
+require 'dialplan.dialplan'
+
+start_dialplan = dialplan.dialplan.Dialplan:new{ log = log, caller = start_caller };
+start_dialplan:configuration_read();
+start_caller.local_node_id = start_dialplan.node_id;
+start_caller:init_channel_variables();
+
+-- session:execute('info','notice');
+
+if not start_dialplan:check_auth() then
+ log:debug('AUTHENTICATION_REQUIRED - domain: ', start_dialplan.domain);
+ start_dialplan:hangup(407, start_dialplan.domain);
+ return false;
+end
+
+-- connect to database
+require 'common.database'
+local database = common.database.Database:new{ log = log }:connect();
+if not database:connected() then
+ log:critical('DIALPLAN_DEFAULT - database connect failed');
+ return;
+end
+
+start_dialplan.database = database;
+
+if start_caller.from_node and not start_dialplan:check_auth_node() then
+ log:debug('AUTHENTICATION_REQUIRED_NODE - node_id: ', start_caller.node_id, ', domain: ', start_dialplan.domain);
+ start_dialplan:hangup(407, start_dialplan.domain);
+else
+ start_destination = { type = 'unknown' }
+ start_caller.session:setHangupHook('hangup_hook_caller', 'destination_number');
+ start_dialplan:run(start_destination);
+end
+
+-- release database handle
+if database then
+ database:release();
+end
diff --git a/misc/freeswitch/scripts/event/call_history_save.lua b/misc/freeswitch/scripts/event/call_history_save.lua
new file mode 100644
index 0000000..057ca16
--- /dev/null
+++ b/misc/freeswitch/scripts/event/call_history_save.lua
@@ -0,0 +1,74 @@
+-- Gemeinschaft 5 module: call_history event handler class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+function handler_class()
+ return CallHistorySave
+end
+
+CallHistorySave = {}
+
+
+function CallHistorySave.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.log = arg.log;
+ self.class = 'callhistorysave'
+ self.database = arg.database;
+ self.domain = arg.domain;
+
+ return object;
+end
+
+
+function CallHistorySave.event_handlers(self)
+ return { CHANNEL_DESTROY = { [true] = self.channel_destroy } }
+end
+
+
+function CallHistorySave.channel_destroy(self, event)
+ local uuid = event:getHeader('Unique-ID');
+ local direction = event:getHeader('variable_direction');
+
+ require 'common.str'
+ local save_cdr = common.str.to_b(event:getHeader('variable_gs_save_cdr'));
+
+ if not save_cdr then
+ self.log:debug('[', uuid,'] CALL_HISTORY_SAVE - event: CHANNEL_DESTROY, direction: ', direction, ', save_cdr: ', save_cdr);
+ return false;
+ end
+
+ require 'common.call_history'
+ call_history_class = common.call_history.CallHistory:new{ log = self.log, database = self.database }
+
+ -- caller entry
+ local account_type = event:getHeader('variable_gs_account_type');
+ local account_id = common.str.to_i(event:getHeader('variable_gs_account_id'));
+
+ if account_type and account_id > 0 and common.str.to_b(event:getHeader('variable_gs_account_node_local')) then
+ call_history_class:insert_event(uuid, account_type, account_id, 'dialed', event);
+ else
+ self.log:info('[', uuid,'] CALL_HISTORY_SAVE - ignore caller entry - account: ', account_type, '=', account_id, ', local: ', event:getHeader('variable_gs_account_node_local'));
+ end
+
+ -- callee entry
+ local account_type = event:getHeader('variable_gs_destination_type');
+ local account_id = common.str.to_i(event:getHeader('variable_gs_destination_id'));
+
+ if account_type and account_id > 0
+ and common.str.to_b(event:getHeader('variable_gs_destination_node_local'))
+ and tostring(event:getHeader('variable_gs_call_service')) ~= 'pickup' then
+
+ if tostring(event:getHeader('variable_endpoint_disposition')) == 'ANSWER' then
+ call_history_class:insert_event(uuid, account_type, account_id, 'received', event);
+ else
+ call_history_class:insert_event(uuid, account_type, account_id, 'missed', event);
+ end
+ else
+ self.log:info('[', uuid,'] CALL_HISTORY_SAVE - ignore callee entry - account: ', account_type, '=', account_id, ', local: ', event:getHeader('variable_gs_destination_node_local'));
+ end
+end
diff --git a/misc/freeswitch/scripts/event/cdr_save.lua b/misc/freeswitch/scripts/event/cdr_save.lua
new file mode 100644
index 0000000..ed53aa3
--- /dev/null
+++ b/misc/freeswitch/scripts/event/cdr_save.lua
@@ -0,0 +1,105 @@
+-- Gemeinschaft 5 module: cdr event handler class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+
+function handler_class()
+ return CdrSave
+end
+
+
+function camelize_type(account_type)
+ ACCOUNT_TYPES = {
+ sipaccount = 'SipAccount',
+ conference = 'Conference',
+ faxaccount = 'FaxAccount',
+ callthrough = 'Callthrough',
+ huntgroup = 'HuntGroup',
+ automaticcalldistributor = 'AutomaticCallDistributor',
+ }
+
+ return ACCOUNT_TYPES[account_type] or account_type;
+end
+
+
+CdrSave = {}
+
+
+function CdrSave.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.log = arg.log;
+ self.class = 'cdrsave'
+ self.database = arg.database;
+ self.domain = arg.domain;
+
+ return object;
+end
+
+
+function CdrSave.event_handlers(self)
+ return { CHANNEL_DESTROY = { [true] = self.channel_destroy } }
+end
+
+
+function CdrSave.channel_destroy(self, event)
+ local uuid = event:getHeader('Unique-ID');
+ local direction = event:getHeader('variable_direction');
+
+ require 'common.str'
+ local save_cdr = common.str.to_b(event:getHeader('variable_gs_save_cdr'));
+
+ if not save_cdr then
+ self.log:debug('[', uuid,'] CDR_SAVE - event: CHANNEL_DESTROY, direction: ', direction, ', save_cdr: ', save_cdr);
+ return false;
+ end
+
+ require 'common.str'
+ local cdr = {}
+
+ cdr.uuid = common.str.to_sql(uuid);
+ cdr.bleg_uuid = common.str.to_sql(event:getHeader('variable_bridge_uuid'));
+ cdr.dialed_number = common.str.to_sql(event:getHeader('Caller-Destination-Number'));
+ cdr.destination_number = common.str.to_sql(event:getHeader('variable_gs_destination_number'));
+ cdr.caller_id_number = common.str.to_sql(event:getHeader('variable_effective_caller_id_number'));
+ cdr.caller_id_name = common.str.to_sql(event:getHeader('variable_effective_caller_id_name'));
+ cdr.callee_id_number = common.str.to_sql(event:getHeader('variable_effective_callee_id_number'));
+ cdr.callee_id_name = common.str.to_sql(event:getHeader('variable_effective_callee_id_name'));
+ cdr.start_stamp = 'FROM_UNIXTIME(' .. math.floor(common.str.to_i(event:getHeader('Caller-Channel-Created-Time')) / 1000000) .. ')';
+ cdr.answer_stamp = 'FROM_UNIXTIME(' .. math.floor(common.str.to_i(event:getHeader('Caller-Channel-Answered-Time')) / 1000000) .. ')';
+ cdr.end_stamp = 'FROM_UNIXTIME(' .. math.floor(common.str.to_i(event:getHeader('Caller-Channel-Hangup-Time')) / 1000000) .. ')';
+ cdr.bridge_stamp = common.str.to_sql(event:getHeader('variable_bridge_stamp'));
+ cdr.duration = common.str.to_sql(event:getHeader('variable_duration'));
+ cdr.billsec = common.str.to_sql(event:getHeader('variable_billsec'));
+ cdr.hangup_cause = common.str.to_sql(event:getHeader('variable_hangup_cause'));
+ cdr.dialstatus = common.str.to_sql(event:getHeader('variable_DIALSTATUS'));
+ cdr.forwarding_number = common.str.to_sql(event:getHeader('variable_gs_forwarding_number'));
+ cdr.forwarding_service = common.str.to_sql(event:getHeader('variable_gs_forwarding_service'));
+ cdr.forwarding_account_id = common.str.to_sql(event:getHeader('variable_gs_auth_account_id'));
+ cdr.forwarding_account_type = common.str.to_sql(camelize_type(event:getHeader('variable_gs_auth_account_type')));
+ cdr.account_id = common.str.to_sql(event:getHeader('variable_gs_account_id'));
+ cdr.account_type = common.str.to_sql(camelize_type(event:getHeader('variable_gs_account_type')));
+ cdr.bleg_account_id = common.str.to_sql(event:getHeader('variable_gs_destination_id'));
+ cdr.bleg_account_type = common.str.to_sql(camelize_type(event:getHeader('variable_gs_destination_type')));
+
+ local keys = {}
+ local values = {}
+
+ for key, value in pairs(cdr) do
+ table.insert(keys, key);
+ table.insert(values, value);
+ end
+
+ self.log:info('[', uuid,'] CDR_SAVE - account: ', cdr.account_type, '=', cdr.account_id,
+ ', caller: ', cdr.caller_id_number, ' ', cdr.caller_id_name,
+ ', callee: ', cdr.callee_id_number, ' ', cdr.callee_id_name,
+ ', cause: ', cdr.hangup_cause
+ );
+
+ local sql_query = 'INSERT INTO `cdrs` (`' .. table.concat(keys, "`, `") .. '`) VALUES (' .. table.concat(values, ", ") .. ')';
+ return self.database:query(sql_query);
+end
diff --git a/misc/freeswitch/scripts/event/event.lua b/misc/freeswitch/scripts/event/event.lua
new file mode 100644
index 0000000..8e67bc9
--- /dev/null
+++ b/misc/freeswitch/scripts/event/event.lua
@@ -0,0 +1,109 @@
+-- Gemeinschaft 5 module: event manager class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+EventManager = {}
+
+-- create event manager object
+function EventManager.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.log = arg.log;
+ self.class = 'eventmanager'
+ self.database = arg.database;
+ self.domain = arg.domain;
+
+ return object;
+end
+
+
+function EventManager.register(self)
+ self.consumer = freeswitch.EventConsumer('all');
+ return (self.consumer ~= nil);
+end
+
+
+function EventManager.load_event_modules(self)
+ local CONFIG_FILE_NAME = '/opt/freeswitch/scripts/ini/events.ini';
+
+ require 'common.configuration_file'
+ self.config = common.configuration_file.get(CONFIG_FILE_NAME);
+
+ return self.config.modules;
+end
+
+
+function EventManager.load_event_handlers(self, event_modules)
+ event_handlers = {}
+
+ for index, event_module_name in ipairs(event_modules) do
+ event_module = require('event.' .. event_module_name);
+ if event_module then
+ self.log:info('[event] EVENT_MANAGER - loading handler module: ', event_module_name);
+ handler_class = event_module.handler_class();
+
+ if handler_class then
+ module_event_handlers = handler_class:new{ log = self.log, database = self.database, domain = self.domain }:event_handlers();
+ if module_event_handlers then
+ for event_name, event_subclasses in pairs(module_event_handlers) do
+ if not event_handlers[event_name] then
+ event_handlers[event_name] = {};
+ end
+
+ for event_subclass, module_event_handler in pairs(event_subclasses) do
+ if not event_handlers[event_name][event_subclass] then
+ event_handlers[event_name][event_subclass] = {};
+ end
+
+ table.insert(event_handlers[event_name][event_subclass], { class = handler_class, method = module_event_handler } );
+ self.log:info('[event] EVENT_MANAGER - module: ', event_module_name, ', handling events: ', event_name, ', subclass:', event_subclass);
+ end
+ end
+ end
+ end
+ end
+ end
+
+ return event_handlers;
+end
+
+
+function EventManager.run(self)
+
+ local event_modules = self:load_event_modules();
+ local event_handlers = self:load_event_handlers(event_modules);
+
+ if not event_handlers then
+ self.log:error('[event] EVENT_MANAGER - no handlers specified');
+ return nil;
+ end
+
+ if not self:register() then
+ return nil;
+ end
+
+ freeswitch.setGlobalVariable('gs_event_manager', 'true');
+ while freeswitch.getGlobalVariable('gs_event_manager') == 'true' do
+ local event = self.consumer:pop(1, 100);
+ if event then
+ local event_type = event:getType();
+ local event_subclass = event:getHeader('Event-Subclass');
+ if event_handlers[event_type] then
+ if event_handlers[event_type][event_subclass] and #event_handlers[event_type][event_subclass] > 0 then
+ for index, event_handler in ipairs(event_handlers[event_type][event_subclass]) do
+ event_handler.method(event_handler.class, event);
+ end
+ end
+ if event_handlers[event_type][true] and #event_handlers[event_type][true] > 0 then
+ for index, event_handler in ipairs(event_handlers[event_type][true]) do
+ event_handler.method(event_handler.class, event);
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/misc/freeswitch/scripts/event/perimeter.lua b/misc/freeswitch/scripts/event/perimeter.lua
new file mode 100644
index 0000000..3babba6
--- /dev/null
+++ b/misc/freeswitch/scripts/event/perimeter.lua
@@ -0,0 +1,106 @@
+-- Gemeinschaft 5 module: cdr event handler class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+
+function handler_class()
+ return Perimeter
+end
+
+
+
+Perimeter = {}
+
+MALICIOUS_CONTACT_COUNT = 20;
+MALICIOUS_CONTACT_TIME_SPAN = 2;
+BAN_FUTILE = 2;
+
+function Perimeter.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.log = arg.log;
+ self.class = 'cdrsave'
+ self.database = arg.database;
+ self.domain = arg.domain;
+
+ self.ip_address_table = {}
+ self:init();
+
+ return object;
+end
+
+
+function Perimeter.event_handlers(self)
+ return { CUSTOM = { ['sofia::pre_register'] = self.sofia_pre_register } }
+end
+
+
+function Perimeter.init(self)
+ local config = common.configuration_file.get('/opt/freeswitch/scripts/ini/perimeter.ini');
+ if config and config.general then
+ self.malicious_contact_count = tonumber(config.general.malicious_contact_count) or MALICIOUS_CONTACT_COUNT;
+ self.malicious_contact_time_span = tonumber(config.general.malicious_contact_time_span) or MALICIOUS_CONTACT_TIME_SPAN;
+ self.ban_futile = tonumber(config.general.ban_futile) or BAN_FUTILE;
+ self.execute = config.general.execute;
+ end
+
+ self.log:info('[perimeter] PERIMETER - setup perimeter defense - config: ', self.malicious_contact_count, '/', self.malicious_contact_time_span, ', execute: ', self.execute);
+end
+
+
+function Perimeter.sofia_pre_register(self, event)
+ local ip_address = event:getHeader('network-ip');
+ self:check_ip(ip_address);
+end
+
+
+function Perimeter.check_ip(self, ip_address)
+ local event_time = os.time();
+
+ if not self.ip_address_table[ip_address] then
+ self.ip_address_table[ip_address] = { last_contact = event_time, contact_count = 0, start_stamp = event_time, banned = 0 }
+ end
+
+ local ip_record = self.ip_address_table[ip_address];
+ ip_record.last_contact = event_time;
+ ip_record.contact_count = ip_record.contact_count + 1;
+
+ if ip_record.contact_count > MALICIOUS_CONTACT_COUNT then
+ if (event_time - ip_record.start_stamp) <= MALICIOUS_CONTACT_TIME_SPAN then
+ self.log:warning('[', ip_address, '] PERIMETER - too many registration attempts');
+ ip_record.start_stamp = event_time;
+ ip_record.contact_count = 0;
+ if ip_record.banned < BAN_FUTILE then
+ ip_record.banned = ip_record.banned + 1;
+ self:ban_ip(ip_address);
+ else
+ self.log:error('[', ip_address, '] PERIMETER - ban futile');
+ end
+ end
+ end
+end
+
+
+function Perimeter.ban_ip(self, ip_address)
+ self.ip_address = ip_address;
+
+ if self.execute then
+ local command = self:expand_variables(self.execute);
+ self.log:debug('[', ip_address, '] PERIMETER - execute: ', command);
+ local result = os.execute(command);
+ if tostring(result) == '0' then
+ self.log:warning('[', ip_address, '] PERIMETER - IP banned');
+ end
+ end
+end
+
+
+function Perimeter.expand_variables(self, line)
+ return (line:gsub('{([%a%d_-]+)}', function(captured)
+ return self[captured];
+ end))
+end
diff --git a/misc/freeswitch/scripts/event/presence_update.lua b/misc/freeswitch/scripts/event/presence_update.lua
new file mode 100644
index 0000000..01ec17b
--- /dev/null
+++ b/misc/freeswitch/scripts/event/presence_update.lua
@@ -0,0 +1,199 @@
+
+module(...,package.seeall)
+
+function handler_class()
+ return PresenceUpdate
+end
+
+ACCOUNT_RECORD_TIMEOUT = 120;
+
+PresenceUpdate = {}
+
+function PresenceUpdate.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.log = arg.log;
+ self.class = 'presenceupdate'
+ self.database = arg.database;
+ self.domain = arg.domain;
+ self.presence_accounts = {}
+ self.account_record = {}
+
+ return object;
+end
+
+
+function PresenceUpdate.event_handlers(self)
+ return {
+ PRESENCE_PROBE = { [true] = self.presence_probe },
+ CUSTOM = { ['sofia::register'] = self.sofia_register, ['sofia::unregister'] = self.sofia_ungerister },
+ PRESENCE_IN = { [true] = self.presence_in },
+ }
+end
+
+
+function PresenceUpdate.presence_probe(self, event)
+ local DIALPLAN_FUNCTION_PATTERN = '^f[_%-].*';
+
+ require 'common.str'
+ local event_to = event:getHeader('to');
+ local event_from = event:getHeader('from');
+ local probe_type = event:getHeader('probe-type');
+ local account, domain = common.str.partition(event_from, '@');
+ local subscription, domain = common.str.partition(event_to, '@');
+
+ self.log:debug('[', account, '] PRESENCE_UPDATE - subscription: ', subscription,', type: ', probe_type);
+ if (not self.presence_accounts[account] or not self.presence_accounts[account][subscription]) and subscription:find(DIALPLAN_FUNCTION_PATTERN) then
+ if not self.presence_accounts[account] then
+ self.presence_accounts[account] = {};
+ end
+ if not self.presence_accounts[account][subscription] then
+ self.presence_accounts[account][subscription] = {};
+ end
+ self:update_function_presence(account, domain, subscription);
+ end
+end
+
+
+function PresenceUpdate.sofia_register(self, event)
+ local account = event:getHeader('from-user');
+ self.log:debug('[', account, '] PRESENCE_UPDATE - flushing account cache on register');
+ self.presence_accounts[account] = nil;
+end
+
+
+function PresenceUpdate.sofia_ungerister(self, event)
+ local account = event:getHeader('from-user');
+ self.log:debug('[', account, '] PRESENCE_UPDATE - flushing account cache on unregister');
+ self.presence_accounts[account] = nil;
+end
+
+
+function PresenceUpdate.presence_in(self, event)
+ if not event:getHeader('status') then
+ return
+ end
+
+ local account, domain = common.str.partition(event:getHeader('from'), '@');
+ local direction = tostring(event:getHeader('presence-call-direction'))
+ local state = event:getHeader('presence-call-info-state');
+ local uuid = event:getHeader('Unique-ID');
+ local caller_id = event:getHeader('Caller-Caller-ID-Number');
+
+ if direction == 'inbound' then
+ self.log:info('[', uuid,'] PRESENCE_INBOUND: account: ', account, ', state: ', state);
+ self:sip_account(true, account, domain, state, uuid);
+ elseif direction == 'outbound' then
+ self.log:info('[', uuid,'] PRESENCE_OUTBOUND: account: ', account, ', state: ', state, ', caller: ', caller_id);
+ self:sip_account(false, account, domain, state, uuid, caller_id);
+ end
+end
+
+
+function PresenceUpdate.update_function_presence(self, account, domain, subscription)
+ local parameters = common.str.to_a(subscription, '_%-');
+ local fid = parameters[2];
+ local function_parameter = parameters[3];
+
+ if not fid then
+ self.log:error('[', account, '] PRESENCE_UPDATE - no function specified');
+ return;
+ end
+
+ if fid == 'cftg' and tonumber(function_parameter) then
+ self:call_forwarding(account, domain, function_parameter);
+ elseif fid == 'hgmtg' then
+ self:hunt_group_membership(account, domain, function_parameter);
+ elseif fid == 'acdmtg' then
+ self:acd_membership(account, domain, function_parameter);
+ end
+
+end
+
+
+function PresenceUpdate.call_forwarding(self, account, domain, call_forwarding_id)
+ require 'common.call_forwarding'
+ local call_forwarding = common.call_forwarding.CallForwarding:new{ log=self.log, database=self.database, domain=domain }:find_by_id(call_forwarding_id);
+
+ require 'common.str'
+ if call_forwarding and common.str.to_b(call_forwarding.record.active) then
+ local destination_type = tostring(call_forwarding.record.call_forwardable_type):lower()
+
+ self.log:debug('[', account, '] PRESENCE_UPDATE - updating call forwarding presence - id: ', call_forwarding_id, ', destination: ', destination_type);
+
+ if destination_type == 'voicemail' then
+ call_forwarding:presence_set('early');
+ else
+ call_forwarding:presence_set('confirmed');
+ end
+ end
+end
+
+
+function PresenceUpdate.hunt_group_membership(self, account, domain, member_id)
+ local sql_query = 'SELECT `active` FROM `hunt_group_members` WHERE `active` IS TRUE AND `id`=' .. tonumber(member_id) .. ' LIMIT 1';
+ local status = self.database:query_return_value(sql_query);
+
+ if status then
+ self.log:debug('[', account, '] PRESENCE_UPDATE - updating hunt group membership presence - id: ', member_id);
+ require 'dialplan.presence'
+ local presence_class = dialplan.presence.Presence:new{
+ log = self.log,
+ database = self.database,
+ domain = domain,
+ accounts = {'f-hgmtg-' .. member_id},
+ uuid = 'hunt_group_member_' .. member_id
+ }:set('confirmed');
+ end
+end
+
+
+function PresenceUpdate.acd_membership(self, account, domain, member_id)
+ local sql_query = 'SELECT `status` FROM `acd_agents` WHERE `status` = "active" AND `id`=' .. tonumber(member_id) .. ' LIMIT 1';
+ local status = self.database:query_return_value(sql_query);
+
+ if status then
+ self.log:debug('[', account, '] PRESENCE_UPDATE - updating ACD membership presence - id: ', member_id);
+ require 'dialplan.presence'
+ local presence_class = dialplan.presence.Presence:new{
+ log = self.log,
+ database = self.database,
+ domain = domain,
+ accounts = {'f-acdmtg-' .. member_id},
+ uuid = 'acd_agent_' .. member_id
+ }:set(status);
+ end
+end
+
+
+function PresenceUpdate.sip_account(self, inbound, account, domain, status, uuid, caller_id)
+ local status_map = { progressing = 'early', alerting = 'confirmed', active = 'confirmed' }
+
+ if not self.account_record[account] or ((os.time() - self.account_record[account].created_at) > ACCOUNT_RECORD_TIMEOUT) then
+ self.log:debug('[', uuid,'] PRESENCE - retrieve account data - account: ', account);
+
+ require 'common.sip_account'
+ local sip_account = common.sip_account.SipAccount:new{ log = self.log, database = self.database }:find_by_auth_name(account);
+
+ if not sip_account then
+ return
+ end
+
+ require 'common.phone_number'
+ local phone_numbers = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database }:list_by_owner(sip_account.id, sip_account.class);
+
+ self.account_record[account] = { id = sip_account.id, class = sip_account.class, phone_numbers = phone_numbers, created_at = os.time() }
+ end
+
+ require 'dialplan.presence'
+ local result = dialplan.presence.Presence:new{
+ log = self.log,
+ database = self.database,
+ inbound = inbound,
+ domain = domain,
+ accounts = self.account_record[account].phone_numbers,
+ uuid = uuid
+ }:set(status_map[status] or 'terminated', caller_id);
+end
diff --git a/misc/freeswitch/scripts/event_manager.lua b/misc/freeswitch/scripts/event_manager.lua
new file mode 100644
index 0000000..0e3c0e0
--- /dev/null
+++ b/misc/freeswitch/scripts/event_manager.lua
@@ -0,0 +1,39 @@
+-- Gemeinschaft 5.0 event handler
+-- (c) AMOOMA GmbH 2012
+--
+
+-- Set logger
+require "common.log"
+local log = common.log.Log:new()
+log.prefix = "#E# "
+
+log:info('[event] EVENT_MANAGER start');
+
+require 'common.database'
+local database = common.database.Database:new{ log = log }:connect();
+if not database:connected() then
+ log:error('[event] EVENT_MANAGER - cannot connect to Gemeinschaft database');
+ return;
+end
+
+require "configuration.sip"
+local sip = configuration.sip.Sip:new{ log = log, database = database }
+
+local domain = '127.0.0.1';
+local domains = sip:domains();
+if domains[1] then
+ domain = domains[1]['host'];
+else
+ log:error('[event] EVENT_MANAGER - No SIP domains found!');
+end
+
+require 'event.event'
+local event_manager = event.event.EventManager:new{ log = log, database = database, domain = domain }
+event_manager:run();
+
+-- ensure database handle is released on exit
+if database then
+ database:release();
+end
+
+log:info('[event] EVENT_MANAGER exit');
diff --git a/misc/freeswitch/scripts/fax_daemon.lua b/misc/freeswitch/scripts/fax_daemon.lua
new file mode 100644
index 0000000..cfe7c4e
--- /dev/null
+++ b/misc/freeswitch/scripts/fax_daemon.lua
@@ -0,0 +1,42 @@
+-- Gemeinschaft 5.0 fax daemon
+-- (c) AMOOMA GmbH 2012
+--
+
+local MAIN_LOOP_SLEEP_TIME = 30;
+
+-- Set logger
+require "common.log"
+local log = common.log.Log:new()
+log.prefix = "### [faxdaemon] "
+
+log:debug('Starting fax daemon');
+
+local database = nil;
+local api = freeswitch.API();
+
+freeswitch.setGlobalVariable('gs_fax_daemon', 'true');
+while freeswitch.getGlobalVariable("gs_fax_daemon") == 'true' do
+ require 'common.database'
+ local database = common.database.Database:new{ log = log }:connect();
+
+ if not database:connected() then
+ log:error("connection to Gemeinschaft database lost - retry in " .. MAIN_LOOP_SLEEP_TIME .. " seconds")
+ else
+ require 'dialplan.fax'
+ local fax_documents = dialplan.fax.Fax:new{log=log, database=database}:queued_for_sending();
+
+ for key, fax_document in pairs(fax_documents) do
+ if table.getn(fax_document.destination_numbers) > 0 and tonumber(fax_document.retry_counter) > 0 then
+ log:debug('FAX_DAEMON_LOOP - fax_document=', fax_document.id, '/', fax_document.uuid, ', number: ' .. fax_document.destination_numbers[1]);
+ local result = api:executeString('luarun send_fax.lua ' .. fax_document.id);
+ end
+ end
+ end
+ database:release();
+
+ if freeswitch.getGlobalVariable("gs_fax_daemon") == 'true' then
+ freeswitch.msleep(MAIN_LOOP_SLEEP_TIME * 1000);
+ end
+end
+
+log:debug('Exiting fax daemon');
diff --git a/misc/freeswitch/scripts/ini/conferences.ini b/misc/freeswitch/scripts/ini/conferences.ini
new file mode 100644
index 0000000..d8d0817
--- /dev/null
+++ b/misc/freeswitch/scripts/ini/conferences.ini
@@ -0,0 +1,27 @@
+; Gemeinschaft 5 conferences configuration file
+; (c) AMOOMA GmbH 2012
+;
+
+[parameters]
+caller-controls = speaker
+moderator-controls = moderator
+max-members = 100
+rate = 16000
+interval = 20
+energy-level = 300
+sound-prefix = /opt/freeswitch/sounds/en/us/callie
+muted-sound = conference/conf-muted.wav
+unmuted-sound = conference/conf-unmuted.wav
+alone-sound = conference/conf-alone.wav
+moh-sound = local_stream://moh
+enter-sound = tone_stream://%(200,0,500,600,700)
+exit-sound = tone_stream://%(500,0,300,200,100,50,25)
+kicked-sound = conference/conf-kicked.wav
+locked-sound = conference/conf-locked.wav
+is-locked-sound = conference/conf-is-locked.wav
+is-unlocked-sound = conference/conf-is-unlocked.wav
+pin-sound = conference/conf-pin.wav
+bad-pin-sound = conference/conf-bad-pin.wav
+caller-id-name = Conference
+caller-id-number =
+comfort-noise = true
diff --git a/misc/freeswitch/scripts/ini/database.ini b/misc/freeswitch/scripts/ini/database.ini
new file mode 100644
index 0000000..1652118
--- /dev/null
+++ b/misc/freeswitch/scripts/ini/database.ini
@@ -0,0 +1,11 @@
+; Gemeinschaft 5 database configuration
+; (c) AMOOMA GmbH 2012
+;
+
+driver = mysql
+
+[mysql]
+host = localhost
+database = gemeinschaft
+user = gemeinschaft
+password = gemeinschaft
diff --git a/misc/freeswitch/scripts/ini/dialplan.ini b/misc/freeswitch/scripts/ini/dialplan.ini
new file mode 100644
index 0000000..f4a6b66
--- /dev/null
+++ b/misc/freeswitch/scripts/ini/dialplan.ini
@@ -0,0 +1,11 @@
+; Gemeinschaft 5 dialplan configuration file
+; (c) AMOOMA GmbH 2012
+;
+
+[parameters]
+node_id = 1
+phone_book_entry_image_url = http://192.168.0.150/uploads/phone_book_entry/image
+user_image_url = http://192.168.0.150/uploads/user/image
+ringtone_url = http://192.168.0.150
+ringback = %(2000,4000,440.0,480.0)
+tone_busy = %(500,500,480,620);loops=4
diff --git a/misc/freeswitch/scripts/ini/events.ini b/misc/freeswitch/scripts/ini/events.ini
new file mode 100644
index 0000000..e63eb73
--- /dev/null
+++ b/misc/freeswitch/scripts/ini/events.ini
@@ -0,0 +1,8 @@
+; Gemeinschaft 5 routing configuration file
+; (c) AMOOMA GmbH 2012
+;
+
+[modules]
+cdr_save
+call_history_save
+presence_update
diff --git a/misc/freeswitch/scripts/ini/gateways.ini.example b/misc/freeswitch/scripts/ini/gateways.ini.example
new file mode 100644
index 0000000..b6ae018
--- /dev/null
+++ b/misc/freeswitch/scripts/ini/gateways.ini.example
@@ -0,0 +1,23 @@
+; Gemeinschaft 5 gateways configuration file
+; (c) AMOOMA GmbH 2012
+;
+
+[gateway1]
+profile = gemeinschaft
+name = gateway1
+username = gateway1
+realm = gemeinschaft
+password = freeswitch
+extension = default
+proxy = 192.168.0.1
+expire-seconds = 600
+register = true
+
+[gateway2]
+profile = gemeinschaft
+name = sipgate
+username = 1234567e0
+password = ABCdeF
+proxy = sipgate.com
+register = true
+extension = {sip_to_user}
diff --git a/misc/freeswitch/scripts/ini/perimeter.ini b/misc/freeswitch/scripts/ini/perimeter.ini
new file mode 100644
index 0000000..ecbb032
--- /dev/null
+++ b/misc/freeswitch/scripts/ini/perimeter.ini
@@ -0,0 +1,9 @@
+; Gemeinschaft 5 perimeter defense configuration file
+; (c) AMOOMA GmbH 2012
+;
+
+[general]
+malicious_contact_count = 20
+malicious_contact_time_span = 2
+ban_futile = 5
+execute = sudo /usr/local/bin/ban_ip.sh {ip_address}
diff --git a/misc/freeswitch/scripts/ini/routes.ini b/misc/freeswitch/scripts/ini/routes.ini
new file mode 100644
index 0000000..1334e7b
--- /dev/null
+++ b/misc/freeswitch/scripts/ini/routes.ini
@@ -0,0 +1,77 @@
+; Gemeinschaft 5 routing configuration file
+; (c) AMOOMA GmbH 2012
+;
+
+[general]
+
+
+[prerouting]
+^%*0%*$ , f-li
+^%*0%*(%d+)#*$ , f-li-%1
+^%*0%*(%d+)%*(%d+)#*$ , f-li-%1-%2
+^#0#$ , f-lo
+^%*30#$ , f-clipon
+^#30#$ , f-clipoff
+^%*31#(%d+)$ , f-dclirof, f-%1
+^#31#(%d+)$ , f-dcliron-%1
+^%*43#$ , f-cwaon
+^#43#$ , f-cwaoff
+^#002#$ , f-cfoff
+^##002#$ , f-cfdel
+^%*21#$ , f-cfu
+^%*21%*(%d+)#$ , f-cfu-%1
+^%*%*21%*(%d+)#$ , f-cfu-%1
+^#21#$ , f-cfuoff
+^##21#$ , f-cfudel
+^%*61#$ , f-cfn
+^%*61%*(%d+)#$ , f-cfn-%1
+^%*%*61%*(%d+)#$ , f-cfn-%1
+^%*61%*(%d+)%*(%d+)#$ , f-cfn-%1-%2
+^%*%*61%*(%d+)%*(%d+)#$ , f-cfn-%1-%2
+^#61#$ , f-cfnoff
+^##61#$ , f-cfndel
+^%*62#$ , f-cfo
+^%*62%*(%d+)#$ , f-cfo-%1
+^%*%*62%*(%d+)#$ , f-cfo-%1
+^#62#$ , f-cfooff
+^##62#$ , f-cfodel
+^%*67#$ , f-cfb
+^%*67%*(%d+)#$ , f-cfb-%1
+^%*%*67%*(%d+)#$ , f-cfb-%1
+^#67#$ , f-cfboff
+^##67#$ , f-cfbdel
+^%*98$ , f-vmcheck
+^%*98#$ , f-vmcheck
+^%*98%*(%d+)#$ , f-vmcheck-%1
+^%*1337%*1%*1#$ , f-loaon
+^%*1337%*1%*0#$ , f-loaoff
+
+^00(%d+)$ , +%1
+^0(%d+)$ , +49%1
+
+
+[outbound]
+^%+(%d+)$ , class=gateway, endpoint=gateway1, group=users, %1
+^([1-9]%d+)$ , class=gateway, endpoint=gateway1, group=users, %1
+
+
+[failover]
+UNALLOCATED_NUMBER = true
+NORMAL_TEMPORARY_FAILURE = true
+
+
+[outbound_cid_number]
+
+
+[outbound_cid_name]
+
+
+[inbound]
+^00(%d+)$ , +%1
+^0(%d+)$ , +49%1
+
+[inbound_cid_number]
+^00(%d+)$ , +%1
+^0(%d+)$ , +49%1
+
+[inbound_cid_name]
diff --git a/misc/freeswitch/scripts/ini/sip_accounts.ini b/misc/freeswitch/scripts/ini/sip_accounts.ini
new file mode 100644
index 0000000..73a5fae
--- /dev/null
+++ b/misc/freeswitch/scripts/ini/sip_accounts.ini
@@ -0,0 +1,10 @@
+; Gemeinschaft 5 sip accounts default parameters
+; (c) AMOOMA GmbH 2012
+;
+
+[parameters]
+vm-enabled = true
+vm-email-all-messages = false
+vm-attach-file = false
+vm-mailto =
+
diff --git a/misc/freeswitch/scripts/ini/sofia.ini b/misc/freeswitch/scripts/ini/sofia.ini
new file mode 100644
index 0000000..9c73990
--- /dev/null
+++ b/misc/freeswitch/scripts/ini/sofia.ini
@@ -0,0 +1,55 @@
+; Gemeinschaft 5 sofia configuration file
+; (c) AMOOMA GmbH 2012
+;
+
+[profiles]
+gemeinschaft
+
+[parameters]
+log-level = 3
+debug-presence = 0
+
+[profile:gemeinschaft]
+user-agent-string = Gemeinschaft5
+debug = 0
+sip-trace = no
+log-auth-failures = false
+context = default
+rfc2833-pt = 101
+pass-rfc2833 = true
+sip-port = 5060
+dialplan = XML
+dtmf-duration = 2000
+rtp-timer-name = soft
+inbound-codec-prefs = PCMA,G7221@32000h,G7221@16000h,G722,PCMU,GSM
+outbound-codec-prefs = PCMA,G7221@32000h,G7221@16000h,G722,PCMU,GSM
+inbound-codec-negotiation = greedy
+ext-rtp-ip = auto-nat
+ext-sip-ip = auto-nat
+hold-music = local_stream://moh
+manage-presence = true
+tls = false
+tls-sip-port = 5061
+tls-cert-dir = /opt/freeswitch/conf/ssl
+accept-blind-reg = false
+accept-blind-auth = false
+nonce-ttl = 60
+disable-transcoding = false
+manual-redirect = true
+disable-transfer = false
+disable-register = false
+auth-calls = false
+inbound-reg-force-matching-username = true
+auth-all-packets = false
+rtp-timeout-sec = 300
+rtp-hold-timeout-sec = 1800
+force-subscription-expires = 3600
+sip-force-expires = 3000
+sip-expires-max-deviation = 600;
+challenge-realm = auto_from
+rtp-rewrite-timestamps = true
+inbound-use-callid-as-uuid = false
+outbound-use-callid-as-uuid = false
+context = default
+record-template = /${record_file}
+odbc-dsn = gemeinschaft:gemeinschaft:gemeinschaft
diff --git a/misc/freeswitch/scripts/phones/phone.lua b/misc/freeswitch/scripts/phones/phone.lua
new file mode 100644
index 0000000..5cd210b
--- /dev/null
+++ b/misc/freeswitch/scripts/phones/phone.lua
@@ -0,0 +1,114 @@
+-- Gemeinschaft 5 module: phone class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+Phone = {}
+
+-- create phone object
+function Phone.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.log = arg.log;
+ self.database = arg.database;
+ return object;
+end
+
+
+
+-- Find a hot-deskable phone by sip-account
+function Phone.find_all_hot_deskable_by_account(self, account_id)
+ require 'common.str'
+
+ local sql_query = 'SELECT \
+ `b`.`id`, `b`.`mac_address`, `b`.`ip_address`, `b`.`http_user`, `b`.`http_password`, `b`.`phoneable_type`, `b`.`phoneable_id`, \
+ `d`.`ieee_name` \
+ FROM `phone_sip_accounts` `a` \
+ JOIN `phones` `b` ON `a`.`phone_id` = `b`.`id` \
+ JOIN `phone_models` `c` ON `b`.`phone_model_id` = `c`.`id` \
+ JOIN `manufacturers` `d` ON `c`.`manufacturer_id` = `d`.`id` \
+ WHERE `b`.`hot_deskable` IS TRUE \
+ AND `c`.`state` = "active" \
+ AND `d`.`state` = "active" \
+ AND `a`.`sip_account_id` = ' .. tonumber(account_id);
+
+ local account_phones = {};
+
+ self.database:query(sql_query, function(account_entry)
+ local phone = Phone:new(self, {object = parent_class});
+ phone.record = account_entry;
+ phone.record.ieee_name = common.str.downcase(account_entry.ieee_name);
+
+ if phone.record.ieee_name == 'snom technology ag' then
+ require 'phones.snom'
+ phone.model = phones.snom.Snom:new();
+ elseif account_entry.ieee_name == 'siemens enterprise communicationsgmbh & co. kg' then
+ require 'phones.siemens'
+ phone.model = phones.siemens.Siemens:new();
+ end
+ table.insert(account_phones, phone);
+ end)
+
+ return account_phones;
+end
+
+
+function Phone.find_hot_deskable_by_account(self, account_id)
+ return self:find_all_hot_deskable_by_account(account_id)[1];
+end
+
+
+function Phone.tenant_id_get(self)
+ local sql_query = 'SELECT `c`.`sip_accountable_id` \
+ FROM `phones` `a` LEFT JOIN `phone_sip_accounts` `b` ON `a`.`id` = `b`.`phone_id` \
+ JOIN `sip_accounts` `c` ON `b`.`sip_account_id` = `c`.`id` AND `sip_accountable_type` = "Tenant" \
+ WHERE `a`.`id` = ' .. tonumber(self.record.id) .. ' LIMIT 1';
+
+ local tenant_id = nil;
+ self.database:query(sql_query, function(tenant_entry)
+ tenant_id = tenant_entry.sip_accountable_id;
+ end)
+
+ return tenant_id;
+end
+
+function Phone.phoneable_set(self, phoneable_id, phoneable_type)
+ sql_query = 'UPDATE `phones` SET `phoneable_type` = "' .. phoneable_type ..'", `phoneable_id` = ' .. phoneable_id .. ' \
+ WHERE `id` = ' .. tonumber(self.record.id);
+ self.database:query(sql_query);
+end
+
+function Phone.logout(self, account_id)
+ local tenant_id = self:tenant_id_get();
+
+ if not tenant_id then
+ self.log:info('PHONE_LOGOUT - tenant not found');
+ return false;
+ end
+
+ self:phoneable_set(tenant_id, 'Tenant');
+
+ sql_query = 'DELETE FROM `phone_sip_accounts` WHERE `sip_account_id` = ' .. tonumber(account_id);
+ return self.database:query(sql_query);
+end
+
+function Phone.login(self, account_id, owner_id, owner_type)
+ self:phoneable_set(owner_id, owner_type);
+ sql_query = 'INSERT INTO `phone_sip_accounts` (`phone_id`, `sip_account_id`, `position`, `created_at`, `updated_at`) \
+ VALUES ('.. tonumber(self.record.id) .. ', '.. tonumber(account_id) .. ', 1, NOW(), NOW())';
+
+ return self.database:query(sql_query);
+end
+
+function Phone.resync(self, arg)
+ if not self.model then
+ self.log:notice('PHONE_RESYNC - unsupported phone model');
+ return false;
+ end
+
+ arg.ip_address = arg.ip_address or self.record.ip_address;
+ return self.model:resync(arg);
+end \ No newline at end of file
diff --git a/misc/freeswitch/scripts/phones/siemens.lua b/misc/freeswitch/scripts/phones/siemens.lua
new file mode 100644
index 0000000..71bb40a
--- /dev/null
+++ b/misc/freeswitch/scripts/phones/siemens.lua
@@ -0,0 +1,45 @@
+-- Gemeinschaft 5 module: general siemens model class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+Siemens = {}
+
+-- create siemens object
+function Siemens.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.log = arg.log;
+ self.PHONE_HTTP_PORT = 8085;
+ return object;
+end
+
+-- send reload message to phone
+function Siemens.resync(self, arg)
+ if arg.ip_address then
+ return self:resync_http(arg.ip_address, arg.http_user, arg.http_password, arg.http_port);
+ end
+
+ return false;
+end
+
+-- send reload message to ip
+function Siemens.resync_http(self, ip_address, http_user, http_password, http_port)
+ local port_str = '';
+ if tonumber(http_port) then
+ port_str = ':' .. http_port;
+ end
+
+ get_command = 'wget --no-proxy -q -O /dev/null -o /dev/null -b --tries=2 --timeout=10 --user="' .. (http_user or '') .. '" --password="' .. (http_password or '') .. '"' ..
+ ' wget http://' .. tostring(ip_address):gsub('[^0-9%.]', '') .. ':' .. (tonumber(http_port) or self.PHONE_HTTP_PORT) .. '/contact_dls.html/ContactDLS' ..
+ ' 1>>/dev/null 2>>/dev/null &';
+
+ result = os.execute(get_command);
+
+ if result and tonumber(result) == 0 then
+ return true;
+ end
+end
diff --git a/misc/freeswitch/scripts/phones/snom.lua b/misc/freeswitch/scripts/phones/snom.lua
new file mode 100644
index 0000000..80d1fce
--- /dev/null
+++ b/misc/freeswitch/scripts/phones/snom.lua
@@ -0,0 +1,65 @@
+-- Gemeinschaft 5 module: general snom model class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+Snom = {}
+
+-- Create Snom object
+function Snom.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.log = arg.log;
+ self.reboot = arg.reboot or true;
+ return object;
+end
+
+-- send reload message to phone
+function Snom.resync(self, arg)
+ if arg.reboot == nil then
+ arg.reboot = self.reboot;
+ end
+
+ local success = nil;
+ if arg.auth_name and arg.domain then
+ success = self:resync_sip(arg.auth_name, arg.domain, arg.reboot);
+ end
+
+ if arg.ip_address and arg.reboot then
+ success = self:resync_http(arg.ip_address, arg.http_user, arg.http_password, arg.http_port);
+ end
+
+ return success;
+end
+
+-- send reload message to sip_account
+function Snom.resync_sip(self, sip_account, domain, reboot)
+ local event = freeswitch.Event('NOTIFY');
+ event:addHeader('profile', 'gemeinschaft');
+ event:addHeader('event-string', 'check-sync;reboot=' .. tostring(reboot));
+ event:addHeader('user', sip_account);
+ event:addHeader('host', domain);
+ event:addHeader('content-type', 'application/simple-message-summary');
+ return event:fire();
+end
+
+-- send reload message to ip
+function Snom.resync_http(self, ip_address, http_user, http_password, http_port)
+ local port_str = '';
+ if tonumber(http_port) then
+ port_str = ':' .. http_port;
+ end
+
+ get_command = 'wget --no-proxy -q -O /dev/null -o /dev/null -b --tries=2 --timeout=10 --user="' .. (http_user or '') .. '" --password="' .. (http_password or '') .. '"' ..
+ ' wget http://' .. tostring(ip_address):gsub('[^0-9%.]', '') .. port_str .. '/advanced.htm?reboot=Reboot' ..
+ ' 1>>/dev/null 2>>/dev/null &';
+
+ result = os.execute(get_command);
+
+ if result and tonumber(result) == 0 then
+ return true;
+ end
+end
diff --git a/misc/freeswitch/scripts/phones/uacsta.lua b/misc/freeswitch/scripts/phones/uacsta.lua
new file mode 100644
index 0000000..61cb788
--- /dev/null
+++ b/misc/freeswitch/scripts/phones/uacsta.lua
@@ -0,0 +1,100 @@
+-- CommonModule: Uacsta
+--
+module(...,package.seeall)
+
+Uacsta = {}
+
+-- Create Uacsta object
+function Uacsta.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self)
+ self.__index = self
+ self.log = arg.log;
+
+ return object
+end
+
+function Uacsta.send(self, sip_account, domain, body)
+ local event = freeswitch.Event("NOTIFY");
+ event:addHeader("profile", "gemeinschaft");
+ event:addHeader("event-string", "uaCSTA");
+ event:addHeader("user", sip_account);
+ event:addHeader("host", domain);
+ event:addHeader("content-type", "application/csta+xml");
+ event:addBody(body);
+ event:fire();
+end
+
+function Uacsta.make_call(self, sip_account, domain, number)
+ local body =
+[[<?xml version="1.0" encoding="UTF-8"?>
+<MakeCall xmlns="http://www.ecma-international.org/standards/ecma-323/csta/ed4">
+ <callingDevice>]] .. sip_account .. [[</callingDevice>
+ <calledDirectoryNumber>]] .. number .. [[</calledDirectoryNumber>
+ <autoOriginate>doNotPrompt</autoOriginate>
+</MakeCall>]]
+
+ self:send(sip_account, domain, body);
+end
+
+function Uacsta.answer_call(self, sip_account, domain)
+ local body =
+[[<?xml version="1.0" encoding="UTF-8"?>
+<AnswerCall xmlns="http://www.ecma-international.org/standards/ecma-323/csta/ed4">
+ <callToBeAnswered>
+ <deviceID>]] .. sip_account .. [[</deviceID>
+ </callToBeAnswered>
+</AnswerCall>]]
+
+ self:send(sip_account, domain, body);
+end
+
+function Uacsta.set_microphone_mute(self, sip_account, domain, value)
+ local body =
+[[<?xml version="1.0" encoding="UTF-8"?>
+<SetMicrophoneMute xmlns="http://www.ecma-international.org/standards/ecma-323/csta/ed3">
+ <device>]] .. sip_account .. [[</device>
+ <auditoryApparatus>1</auditoryApparatus>
+ <microphoneMuteOn>]] .. tostring(value) .. [[</microphoneMuteOn>
+</SetMicrophoneMute>]]
+
+ self:send(sip_account, domain, body);
+end
+
+function Uacsta.set_speaker_volume(self, sip_account, domain, value)
+ local body =
+[[<?xml version="1.0" encoding="UTF-8"?>
+<SetSpeakerVolume xmlns="http://www.ecma-international.org/standards/ecma-323/csta/ed3">
+ <device>]] .. sip_account .. [[</device>
+ <auditoryApparatus>1</auditoryApparatus>
+ <speakerVolume>]] .. tonumber(value) .. [[</speakerVolume>
+</SetSpeakerVolume>]]
+
+ self:send(sip_account, domain, body);
+end
+
+function Uacsta.set_do_not_disturb(self, sip_account, domain, value)
+ local body =
+[[<?xml version="1.0" encoding="UTF-8"?>
+<SetDoNotDisturb xmlns="http://www.ecma-international.org/standards/ecma-323/csta/ed3">
+ <device>]] .. sip_account .. [[</device>
+ <doNotDisturbOn>]] .. tostring(value) .. [[</doNotDisturbOn>
+</SetDoNotDisturb>]]
+
+ self:send(sip_account, domain, body);
+end
+
+function Uacsta.set_forwarding(self, sip_account, domain, forwarding_type, number, activate)
+ local forwarding_types = { "forwardImmediate", "forwardBusy", "forwardNoAns" }
+ local body =
+[[<?xml version="1.0" encoding="UTF-8"?>
+<SetForwarding xmlns="http://www.ecma-international.org/standards/ecma-323/csta/ed3">
+ <device>]] .. sip_account .. [[</device>
+ <forwardingType>]] .. tostring(forwarding_types[tonumber(forwarding_type)]) .. [[</forwardingType>
+ <forwardDN>]] .. number .. [[</forwardDN>
+ <activateForward>]] .. tostring(activate) .. [[</activateForward>
+</SetForwarding>]]
+
+ self:send(sip_account, domain, body);
+end
diff --git a/misc/freeswitch/scripts/send_fax.lua b/misc/freeswitch/scripts/send_fax.lua
new file mode 100644
index 0000000..321a5b1
--- /dev/null
+++ b/misc/freeswitch/scripts/send_fax.lua
@@ -0,0 +1,170 @@
+-- Gemeinschaft 5.0
+-- (c) AMOOMA GmbH 2012
+--
+
+local FAX_FILE_PATH = "/opt/GS5/public/uploads/fax_document/tiff/";
+local FAX_ANSWERING_TIMEOUT = 20;
+
+-- Set logger
+require "common.log"
+local log = common.log.Log:new()
+log.prefix = "### [sendfax] "
+
+local document_id = argv[1];
+
+require 'common.database'
+local database = common.database.Database:new{ log = log }:connect();
+
+if not database:connected() then
+ log:error('cannot connect to Gemeinschaft database');
+ return
+end
+
+if not tonumber(document_id) then
+ log:error('document id not specified');
+ return
+end
+
+local defaults = {log=log, database=database}
+require "dialplan.fax"
+local fax_class = dialplan.fax.Fax:new(defaults);
+
+local fax_document = fax_class:find_document_by_id(document_id);
+
+if not fax_document then
+ log:error('document ' .. document_id .. ' not found');
+ return
+end
+
+if tonumber(fax_document.retry_counter) > 0 then
+ fax_class:document_update(document_id, {state = 'sending', retry_counter = fax_document.retry_counter - 1});
+else
+ fax_class:document_update(document_id, {state = 'sending'});
+end
+
+local fax_account = fax_class:find_by_id(fax_document.fax_account_id);
+
+if not fax_account then
+ log:error('fax account ' .. fax_document.fax_account_id .. ' not found');
+ return
+end
+
+local destination_number = fax_class:destination_number(document_id);
+
+if not destination_number or tostring(destination_number) == '' then
+ log:error('destination number not found');
+ return
+end
+
+require 'common.str'
+destination_number = common.str.strip(destination_number);
+
+log:info('FAX_SEND - fax_document=' .. document_id .. ', destination number: ' .. destination_number .. ', retries: ' .. fax_document.retry_counter);
+
+require "common.phone_number"
+local phone_number_class = common.phone_number.PhoneNumber:new(defaults);
+
+phone_number = phone_number_class:find_by_number(destination_number);
+
+local origination_variables = {
+ 'gs_account_id=' .. fax_account.record.id,
+ 'gs_account_uuid=' .. fax_account.record.uuid,
+ 'gs_account_type=' .. 'faxaccount',
+ 'gs_auth_account_id=' .. fax_account.record.id,
+ 'gs_auth_account_uuid=' .. fax_account.record.uuid,
+ 'gs_auth_account_type=' .. 'faxaccount',
+}
+
+local session = nil
+
+if phone_number then
+ session = freeswitch.Session("[" .. table.concat(origination_variables, ",") .. "]loopback/" .. destination_number .. "/default");
+else
+ local owner_class = common.str.downcase(fax_account.record.fax_accountable_type);
+
+ local caller = {}
+ caller.caller_phone_numbers = phone_number_class:list_by_owner(fax_account.record.id, 'FaxAccount');
+ caller.account = fax_account;
+ caller.auth_account = fax_account;
+ caller.caller_id_name = fax_account.record.station_id;
+
+ if owner_class == 'user' then
+ require 'dialplan.user'
+ caller.auth_account.owner = dialplan.user.User:new{ log = log, database = database }:find_by_id(fax_account.record.fax_accountable_id);
+ if caller.auth_account.owner then
+ caller.auth_account.owner.groups = caller.auth_account.owner:list_groups();
+ end
+ elseif owner_class == 'tenant' then
+ require 'dialplan.tenant'
+ caller.auth_account.owner = dialplan.tenant.Tenant:new{ log = log, database = database }:find_by_id(fax_account.record.fax_accountable_id);
+ end
+
+ require 'common.configuration_file'
+ local routing_table = common.configuration_file.get('/opt/freeswitch/scripts/ini/routes.ini');
+ require 'dialplan.route'
+ local routes = dialplan.route.Route:new{ log = log, database = database, routing_table = routing_table }:outbound(caller, destination_number);
+
+ for index, route in ipairs(routes) do
+ log:info('FAX_SEND - ', route.class, '=', route.endpoint, ', number: ', route.value);
+ if route.class == 'gateway' then
+ table.insert(origination_variables, "origination_caller_id_number='" .. (route.caller_id_number or caller.caller_phone_numbers[1]) .. "'");
+ table.insert(origination_variables, "origination_caller_id_name='" .. (route.caller_id_name or fax_account.record.station_id) .. "'");
+ session = freeswitch.Session('[' .. table.concat(origination_variables, ',') .. ']sofia/gateway/' .. route.endpoint .. '/' .. route.value);
+ break;
+ end
+ end
+end
+
+local loop_count = FAX_ANSWERING_TIMEOUT;
+local cause = "UNSPECIFIED"
+
+while session and session:ready() and not session:answered() and loop_count >= 0 do
+ log:debug('waiting for answer: ' .. loop_count)
+ loop_count = loop_count - 1;
+ freeswitch.msleep(1000);
+end
+
+if session and session:answered() then
+ log:info('FAX_SEND - sending fax_document=' .. fax_document.id .. ' (' .. fax_document.tiff .. ')');
+
+ local file_name = FAX_FILE_PATH .. fax_document.id .. "/" .. fax_document.tiff;
+
+ session:setVariable('fax_ident', fax_account.record.station_id)
+ session:setVariable('fax_header', fax_account.record.name)
+ session:setVariable('fax_verbose', 'false')
+ local start_time = os.time();
+ session:execute('txfax', file_name);
+
+ fax_state = {
+ state = nil,
+ transmission_time = os.time() - start_time,
+ document_total_pages = common.str.to_i(session:getVariable('fax_document_total_pages')),
+ document_transferred_pages = common.str.to_i(session:getVariable('fax_document_transferred_pages')),
+ ecm_requested = common.str.to_b(session:getVariable('fax_ecm_requested')),
+ ecm_used = common.str.to_b(session:getVariable('fax_ecm_used')),
+ image_resolution = common.str.to_s(session:getVariable('fax_image_resolution')),
+ image_size = common.str.to_i(session:getVariable('fax_image_size')),
+ local_station_id = common.str.to_s(session:getVariable('fax_local_station_id')),
+ result_code = common.str.to_i(session:getVariable('fax_result_code')),
+ remote_station_id = common.str.to_s(session:getVariable('fax_remote_station_id')),
+ success = common.str.to_b(session:getVariable('fax_success')),
+ transfer_rate = common.str.to_i(session:getVariable('fax_transfer_rate')),
+ }
+
+ if fax_state.success then
+ fax_state.state = 'successful';
+ else
+ fax_state.state = 'unsuccessful';
+ end
+
+ fax_account:document_update(fax_document.id, fax_state)
+
+ cause = session:hangupCause();
+ log:info('FAX_SEND - end - fax_document=', fax_document.id, ', success: ', fax_state.state, ', cause: ', cause, ', result: ', fax_state.result_code, ' ', session:getVariable('fax_result_text'));
+else
+ if session then
+ cause = session:hangupCause();
+ end
+ log:debug('Destination "', destination_number, '" could not be reached, cause: ', cause)
+ fax_account:document_update(fax_document.id, {state = 'unsuccessful', result_code = "129"})
+end
diff --git a/misc/mon_ami/asterisk.py b/misc/mon_ami/asterisk.py
new file mode 100644
index 0000000..ffcff06
--- /dev/null
+++ b/misc/mon_ami/asterisk.py
@@ -0,0 +1,299 @@
+# -*- coding: utf-8 -*-
+# MonAMI Asterisk Manger Interface Server
+# Asterisk AMI client connector
+# (c) AMOOMA GmbH 2012
+
+from threading import Thread, Lock
+from log import ldebug, linfo, lwarn, lerror, lcritic
+from time import sleep
+from traceback import format_exc
+from helper import to_hash
+import socket
+
+class AsteriskAMIServer(Thread):
+
+ def __init__(self, client_socket, address, message_queue):
+ Thread.__init__(self)
+ self.runthread = True
+ self.LINE_SEPARATOR = "\r\n"
+ self.GREETING_STRING = 'Asterisk Call Manager/1.1'
+ self.ASTERISK_VERSION_STRING = 'Asterisk 1.6.2.9-2'
+ self.ASTERISK_CHANNEL_STATES = (
+ 'Down',
+ 'Reserved',
+ 'Offhook',
+ 'Dialing',
+ 'Ring',
+ 'Ringing',
+ 'Up',
+ 'Busy',
+ 'Dialing_Offhook',
+ 'Pprering',
+ 'Mute',
+ )
+ self.ASTERISK_PRESENTATION_INDICATOR = (
+ 'Presentation allowed',
+ 'Presentation restricted',
+ 'Number not available due to interworking',
+ 'Reserved',
+ )
+ self.ASTERISK_SCREENING_INDICATOR = (
+ 'not screened',
+ 'verified and passed',
+ 'verified and failed',
+ 'Network provided',
+ )
+
+ self.write_lock = Lock()
+ self.socket = client_socket
+ self.address = address
+ self.message_queue = message_queue
+
+
+ def stop(self):
+ ldebug('thread stop', self)
+ self.runthread = False
+
+
+ def run(self):
+ ldebug('starting AMI server thread', self)
+
+ data = ''
+ while self.runthread and self.socket:
+ try:
+ recv_buffer = self.socket.recv(128)
+ except socket.timeout as exception:
+ # Socket timeout occured
+ continue
+ except:
+ lerror(format_exc(), self)
+ self.runthread = False
+ break
+
+ if not recv_buffer:
+ ldebug('client connection lost', self)
+ break
+
+ data += recv_buffer
+ messages = data.split(self.LINE_SEPARATOR * 2)
+ data = messages.pop()
+
+ for message_str in messages:
+ if not message_str:
+ continue
+
+ message = to_hash(message_str.split(self.LINE_SEPARATOR))
+ self.message_queue.appendleft({'type': 'ami_client_message', 'body': message})
+
+ ldebug('exiting AMI server thread', self)
+
+
+ def send(self, send_buffer):
+ try:
+ self.write_lock.acquire()
+ self.socket.send(send_buffer)
+ self.write_lock.release()
+ return True
+ except:
+ return False
+
+
+ def send_message(self, *message):
+ if len(message) == 1 and type(message[0]) == list:
+ self.send(self.LINE_SEPARATOR.join(message[0]) + (self.LINE_SEPARATOR * 2))
+ else:
+ self.send(self.LINE_SEPARATOR.join(message) + (self.LINE_SEPARATOR * 2))
+
+ def send_greeting(self):
+ self.send_message(self.GREETING_STRING)
+
+ def send_message_unknown(self, command):
+ self.send_message('Response: Error', 'Message: Invalid/unknown command: %s.' % command)
+
+ def send_login_ack(self):
+ self.send_message('Response: Success', 'Message: Authentication accepted')
+
+ def send_login_nack(self):
+ self.send_message('Response: Error', 'Message: Authentication failed')
+
+ def send_logout_ack(self):
+ self.send_message('Response: Goodbye', 'Message: Thank you for flying MonAMI')
+
+ def send_pong(self, action_id):
+ self.send_message('Response: Pong', "ActionID: %s" % str(action_id), 'Server: localhost')
+
+ def send_asterisk_version(self, action_id):
+ self.send_message(
+ 'Response: Follows',
+ 'Privilege: Command',
+ "ActionID: %s" % str(action_id),
+ self.ASTERISK_VERSION_STRING,
+ '--END COMMAND--'
+ )
+
+ def send_hangup_ack(self):
+ self.send_message('Response: Success', 'Message: Channel Hungup')
+
+
+ def send_originate_ack(self, action_id):
+ self.send_message('Response: Success', "ActionID: %s" % str(action_id), 'Message: Originate successfully queued')
+
+
+ def send_status_ack(self, action_id):
+ self.send_message(
+ 'Response: Success',
+ "ActionID: %s" % str(action_id),
+ 'Message: Channel status will follow'
+ )
+ self.send_message(
+ 'Event: StatusComplete',
+ "ActionID: %s" % action_id,
+ 'Items: 0'
+ )
+
+ def send_extension_state(self, action_id, extension, context = 'default', status = -1, hint = ''):
+ self.send_message(
+ 'Response: Success',
+ "ActionID: %s" % str(action_id),
+ 'Message: Extension Status',
+ 'Exten: %s' % extension,
+ 'Context: %s' % context,
+ 'Hint: %s' % hint,
+ 'Status: %d' % status,
+ )
+
+
+ def send_event_newchannel(self, uuid, channel_name, channel_state, caller_id_number = '', caller_id_name = '', destination_number = ''):
+ self.send_message(
+ 'Event: Newchannel',
+ 'Privilege: call,all',
+ 'Channel: %s' % str(channel_name),
+ 'ChannelState: %d' % channel_state,
+ 'ChannelStateDesc: %s' % self.ASTERISK_CHANNEL_STATES[channel_state],
+ 'CallerIDNum: %s' % str(caller_id_number),
+ 'CallerIDName: %s' % str(caller_id_name),
+ 'AccountCode:',
+ 'Exten: %s' % str(destination_number),
+ 'Context: default',
+ 'Uniqueid: %s' % str(uuid),
+ )
+
+
+ def send_event_newstate(self, uuid, channel_name, channel_state, caller_id_number = '', caller_id_name = ''):
+ self.send_message(
+ 'Event: Newstate',
+ 'Privilege: call,all',
+ 'Channel: %s' % str(channel_name),
+ 'ChannelState: %d' % channel_state,
+ 'ChannelStateDesc: %s' % self.ASTERISK_CHANNEL_STATES[channel_state],
+ 'CallerIDNum: %s' % str(caller_id_number),
+ 'CallerIDName: %s' % str(caller_id_name),
+ 'Uniqueid: %s' % str(uuid),
+ )
+
+
+ def send_event_newcallerid(self, uuid, channel_name, caller_id_number = '', caller_id_name = '', calling_pres = 0):
+
+ presentation = self.ASTERISK_PRESENTATION_INDICATOR[calling_pres >> 6]
+ screening = self.ASTERISK_SCREENING_INDICATOR[calling_pres & 3]
+
+ self.send_message(
+ 'Event: NewCallerid',
+ 'Privilege: call,all',
+ 'Channel: %s' % str(channel_name),
+ 'CallerIDNum: %s' % str(caller_id_number),
+ 'CallerIDName: %s' % str(caller_id_name),
+ 'Uniqueid: %s' % str(uuid),
+ 'CID-CallingPres: %d (%s, %s)' % (calling_pres, presentation, screening),
+ )
+
+
+ def send_event_hangup(self, uuid, channel_name, caller_id_number = '', caller_id_name = '', cause = 0):
+ self.send_message(
+ 'Event: Hangup',
+ 'Privilege: call,all',
+ 'Channel: %s' % str(channel_name),
+ 'CallerIDNum: %s' % str(caller_id_number),
+ 'CallerIDName: %s' % str(caller_id_name),
+ 'Cause: %d' % cause,
+ 'Cause-txt: Unknown',
+ 'Uniqueid: %s' % str(uuid)
+ )
+
+
+ def send_event_dial_begin(self, uuid, channel_name, caller_id_number, caller_id_name, destination_channel, destination_uuid, destination_number):
+ self.send_message(
+ 'Event: Dial',
+ 'Privilege: call,all',
+ 'SubEvent: Begin',
+ "Channel: %s" % str(channel_name),
+ "Destination: %s" % str(destination_channel),
+ 'CallerIDNum: %s' % str(caller_id_number),
+ 'CallerIDName: %s' % str(caller_id_name),
+ 'Uniqueid: %s' % str(uuid),
+ 'DestUniqueid: %s' % str(destination_uuid),
+ 'Dialstring: %s@default' % str(destination_number)
+ )
+
+
+ def send_event_dial_end(self, uuid, channel_name, dial_status = 'UNKNOWN'):
+ self.send_message(
+ 'Event: Dial',
+ 'Privilege: call,all',
+ 'SubEvent: End',
+ "Channel: %s" % str(channel_name),
+ 'Uniqueid: %s' % str(uuid),
+ "DialStatus: %s" % str(dial_status),
+ )
+
+
+ def send_event_originate_response(self, uuid, channel_name, caller_id_number, caller_id_name, destination_number, action_id, reason):
+ #reasons:
+ #0: no such extension or number
+ #1: no answer
+ #4: answered
+ #8: congested or not available
+
+ if reason == 4:
+ response = 'Success'
+ else:
+ response = 'Failure'
+
+ self.send_message(
+ 'Event: OriginateResponse',
+ 'Privilege: call,all',
+ 'ActionID: %s' % str(action_id),
+ 'Response: %s' % response,
+ 'Channel: %s' % str(channel_name),
+ 'Context: default',
+ 'Exten: %s' % str(destination_number),
+ 'Reason: %d' % reason,
+ 'CallerIDNum: %s' % str(caller_id_number),
+ 'CallerIDName: %s' % str(caller_id_name),
+ 'Uniqueid: %s' % str(uuid),
+ )
+
+
+ def send_event_bridge(self, uuid, channel_name, caller_id, o_uuid, o_channel_name, o_caller_id):
+ self.send_message(
+ 'Event: Bridge',
+ 'Privilege: call,all',
+ 'Bridgestate: Link',
+ 'Bridgetype: core',
+ 'Channel1: %s' % str(channel_name),
+ 'Channel2: %s' % str(o_channel_name),
+ 'Uniqueid1: %s' % str(uuid),
+ 'Uniqueid2: %s' % str(o_uuid),
+ 'CallerID1: %s' % str(caller_id),
+ 'CallerID2: %s' % str(o_caller_id),
+ )
+
+ def send_event_newaccountcode(self, uuid, channel_name):
+ self.send_message(
+ 'Event: NewAccountCode',
+ 'Privilege: call,all',
+ "Channel: %s" % str(channel_name),
+ 'Uniqueid: %s' % str(uuid),
+ 'AccountCode:',
+ 'OldAccountCode:',
+ )
diff --git a/misc/mon_ami/freeswitch.py b/misc/mon_ami/freeswitch.py
new file mode 100644
index 0000000..eab9bb6
--- /dev/null
+++ b/misc/mon_ami/freeswitch.py
@@ -0,0 +1,194 @@
+# -*- coding: utf-8 -*-
+# MonAMI Asterisk Manger Interface server
+# FreeSWITCH event socket interface
+# (c) AMOOMA GmbH 2012
+
+from threading import Thread, Lock
+from log import ldebug, linfo, lwarn, lerror, lcritic
+from collections import deque
+from time import sleep, time
+from random import random
+from helper import to_hash
+from traceback import format_exc
+import socket
+import sys
+import hashlib
+
+
+class FreeswitchEventSocket(Thread):
+
+ def __init__(self, host, port, password):
+ Thread.__init__(self)
+ self.LINE_SEPARATOR = "\n"
+ self.SOCKET_TIMEOUT = 1
+ self.MESSAGE_PIPE_MAX_LENGTH = 128
+ self.write_lock = Lock()
+ self.host = host
+ self.port = port
+ self.password = password
+ self.runthread = True
+ self.fs = None
+ self.client_queues = {}
+
+
+ def stop(self):
+ ldebug('thread stop', self)
+ self.runthread = False
+
+
+ def run(self):
+ ldebug('starting FreeSWITCH event_socket thread', self)
+
+ while self.runthread:
+ if not self.connect():
+ ldebug('could not connect to FreeSWITCH - retry', self)
+ sleep(self.SOCKET_TIMEOUT)
+ continue
+ ldebug('opening event_socket connection', self)
+
+ data = ''
+ while self.runthread and self.fs:
+
+ try:
+ recv_buffer = self.fs.recv(128)
+ except socket.timeout as exception:
+ # Socket timeout occured
+ continue
+ except:
+ lerror(format_exc(), self)
+ self.runthread = False
+ break
+
+ if not recv_buffer:
+ ldebug('event_socket connection lost', self)
+ break
+
+ data += recv_buffer
+ messages = data.split(self.LINE_SEPARATOR * 2)
+ data = messages.pop()
+
+ for message_str in messages:
+ if not message_str:
+ continue
+ message_body = None
+
+ message = to_hash(message_str.split(self.LINE_SEPARATOR))
+
+ if not 'Content-Type' in message:
+ ldebug('message without Content-Type', self)
+ continue
+
+ if 'Content-Length' in message and int(message['Content-Length']) > 0:
+ content_length = int(message['Content-Length'])
+ while len(data) < int(message['Content-Length']):
+ try:
+ data += self.fs.recv(content_length - len(data))
+ except socket.timeout as exception:
+ ldebug('Socket timeout in message body', self)
+ continue
+ except:
+ lerror(format_exc(), self)
+ break
+ message_body = data.strip()
+ data = ''
+ else:
+ content_length = 0
+
+ self.process_message(message['Content-Type'], message, content_length, message_body)
+
+
+ ldebug('closing event_socket connection', self)
+ if self.fs:
+ self.fs.close()
+
+
+ def connect(self):
+ fs = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ try:
+ fs.connect((self.host, self.port))
+ except:
+ lerror(format_exc(), self)
+ return False
+
+ fs.settimeout(self.SOCKET_TIMEOUT)
+ self.fs = fs
+ return True
+
+
+ def authenticate(self):
+ ldebug('send authentication to FreeSWITCH', self)
+ self.send_message("auth %s" % self.password)
+
+
+ def send(self, send_buffer):
+ try:
+ self.write_lock.acquire()
+ self.fs.send(send_buffer)
+ self.write_lock.release()
+ return True
+ except:
+ return False
+
+
+ def send_message(self, *message):
+ if len(message) == 1 and type(message[0]) == list:
+ self.send(self.LINE_SEPARATOR.join(message[0]) + (self.LINE_SEPARATOR * 2))
+ else:
+ self.send(self.LINE_SEPARATOR.join(message) + (self.LINE_SEPARATOR * 2))
+
+
+ def process_message(self, content_type, message_head, content_length, message_body):
+
+ if content_type == 'auth/request':
+ self.authenticate()
+ if content_type == 'command/reply':
+ if 'Reply-Text' in message_head:
+ ldebug('FreeSWITCH command reply: %s' % message_head['Reply-Text'], self)
+ elif content_type == 'text/event-plain':
+ event = to_hash(message_body.split(self.LINE_SEPARATOR))
+
+ if 'Event-Name' in event and event['Event-Name'] in self.client_queues:
+ event_type = event['Event-Name']
+ for entry_id, message_pipe in self.client_queues[event_type].items():
+ if type(message_pipe) == deque:
+ if len(message_pipe) < self.MESSAGE_PIPE_MAX_LENGTH:
+ message_pipe.appendleft({'type': 'freeswitch_event', 'body': event})
+ else:
+ lwarn("event queue %d full" % entry_id)
+ else:
+ ldebug("force-deregister event queue %d for event type %s" % (entry_id, event_type), self)
+ del self.client_queues[event_type][entry_id]
+
+ def register_client_queue(self, queue, event_type):
+ if not event_type in self.client_queues:
+ self.client_queues[event_type] = {}
+ self.send_message("event plain all %s" % event_type)
+ ldebug("we are listening now to events of type: %s" % event_type, self)
+ self.client_queues[event_type][id(queue)] = queue
+ ldebug("event queue %d registered for event type: %s" % (id(queue), event_type), self)
+
+
+ def deregister_client_queue(self, queue, event_type):
+ ldebug("deregister event queue %d for event type %s" % (id(queue), event_type), self)
+ del self.client_queues[event_type][id(queue)]
+
+ def deregister_client_queue_all(self, queue):
+ for event_type, event_queues in self.client_queues.items():
+ if id(queue) in event_queues:
+ ldebug("deregister event queue %d for all registered event types - event type %s" % (id(queue), event_type), self)
+ del self.client_queues[event_type][id(queue)]
+
+
+ def hangup(self, uuid, hangup_cause = 'NORMAL_CLEARING'):
+ ldebug('hangup channel: %s' % uuid, self)
+ self.send_message('SendMsg %s' % uuid, 'call-command: hangup', 'hangup-cause: %s' % hangup_cause)
+
+ return True
+
+
+ def originate(self, sip_account, extension, action_id = ''):
+ uuid = hashlib.md5('%s%f' % (sip_account, random() * 65534)).hexdigest()
+ ldebug('originate call - from: %s, to: %s, uuid: %s' % (sip_account, extension, uuid), self)
+ self.send_message('bgapi originate {origination_uuid=%s,origination_action=%s,origination_caller_id_number=%s}user/%s %s' % (uuid, action_id, sip_account, sip_account, extension))
+
+ return uuid
diff --git a/misc/mon_ami/helper.py b/misc/mon_ami/helper.py
new file mode 100644
index 0000000..bf286de
--- /dev/null
+++ b/misc/mon_ami/helper.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# MonAMI Asterisk Manger Interface server
+# helper functions
+# (c) AMOOMA GmbH 2012
+
+
+def to_hash(message):
+ message_hash = {}
+ for line in message:
+ keyword, delimeter, value = line.partition(": ")
+ if (keyword):
+ message_hash[keyword] = value.strip()
+
+ return message_hash
+
+
+def sval(array, key):
+ try:
+ return array[key]
+ except:
+ return None
diff --git a/misc/mon_ami/log.py b/misc/mon_ami/log.py
new file mode 100644
index 0000000..92709ad
--- /dev/null
+++ b/misc/mon_ami/log.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+# Log library
+# (c) AMOOMA GmbH 2012
+
+import logging
+
+def ldebug(entry, initiator = None):
+ global logger
+ logger.debug('%s(%d) %s' % (type(initiator).__name__, id(initiator), entry))
+
+def lwarn(entry, initiator = None):
+ global logger
+ logger.warning('%s(%d) %s' % (type(initiator).__name__, id(initiator), entry))
+
+def lerror(entry, initiator = None):
+ global logger
+ logger.error('%s(%d) %s' % (type(initiator).__name__, id(initiator), entry))
+
+def linfo(entry, initiator = None):
+ global logger
+ logger.info('%s(%d) %s' % (type(initiator).__name__, id(initiator), entry))
+
+def lcritic(entry, initiator = None):
+ global logger
+ logger.critical('%s(%d) %s' % (type(initiator).__name__, id(initiator), entry))
+
+def setup_log(file_name = None, loglevel = 5, logformat = None):
+ from sys import stdout
+ global logger
+
+ if file_name:
+ try:
+ logfile = logging.FileHandler(file_name)
+ except:
+ logfile = logging.StreamHandler(stdout)
+ else: logfile = logging.StreamHandler(stdout)
+
+ loglevel = int(loglevel)
+
+ if (loglevel == 0):
+ logfile.setLevel(logging.NOTSET)
+ logger.setLevel(logging.NOTSET)
+ elif (loglevel == 1):
+ logfile.setLevel(logging.CRITICAL)
+ logger.setLevel(logging.CRITICAL)
+ elif (loglevel == 2):
+ logfile.setLevel(logging.ERROR)
+ logger.setLevel(logging.ERROR)
+ elif (loglevel == 3):
+ logfile.setLevel(logging.WARNING)
+ logger.setLevel(logging.WARNING)
+ elif (loglevel == 4):
+ logfile.setLevel(logging.INFO)
+ logger.setLevel(logging.INFO)
+ elif (loglevel >= 5):
+ logfile.setLevel(logging.DEBUG)
+ logger.setLevel(logging.DEBUG)
+
+ if not logformat:
+ logformat = '%(asctime)s-%(name)s-%(levelname)s-%(message)s'
+
+ try:
+ format = logging.Formatter(logformat)
+ logfile.setFormatter(format)
+ except:
+ format = logging.Formatter('%(asctime)s-%(name)s-%(levelname)s-%(message)s')
+ logfile.setFormatter(format)
+
+ logger.addHandler(logfile)
+
+logger = logging.getLogger('#')
diff --git a/misc/mon_ami/mon-ami b/misc/mon_ami/mon-ami
new file mode 100755
index 0000000..a630140
--- /dev/null
+++ b/misc/mon_ami/mon-ami
@@ -0,0 +1,58 @@
+#!/bin/sh
+
+#####################################################################
+# MonAMI Asterisk Manger Interface Emulator
+# Start Script
+# (c) AMOOMA GmbH 2012
+#####################################################################
+
+### BEGIN INIT INFO
+# Provides: mon_ami
+# Required-Start: freeswitch
+# Required-Stop: freeswitch
+# Default-Start: 2 3 4 5
+# Default-Stop: 0 1 6
+# Short-Description: MonAMI Asterisk Manger Interface Emulator
+# Description:
+#
+### END INIT INFO
+
+DAEMON=/opt/GS5/misc/mon_ami/mon_ami
+EXECUTABLE=`basename 'mon_ami'`
+DESC="MonAMI Asterisk Manger Interface Emulator"
+ARGS="--log-file=/var/log/mon_ami.log"
+
+if ! [ -x $DAEMON ] ; then
+ echo "ERROR: $DAEMON not found"
+ exit 1
+fi
+
+case "$1" in
+ start)
+ echo -n "Starting $DESC: "
+ start-stop-daemon --start --pidfile /var/run/$EXECUTABLE.pid \
+ --make-pidfile --background --startas $DAEMON -- $ARGS
+ echo "$EXECUTABLE."
+ ;;
+
+ stop)
+ echo -n "Stopping $DESC: "
+ start-stop-daemon --stop --quiet --oknodo --retry=TERM/30/KILL/5 \
+ --pidfile /var/run/$EXECUTABLE.pid
+ rm -f /var/run/$NAME.pid
+ echo "$EXECUTABLE."
+ ;;
+
+ reload|restart|force-reload)
+ $0 stop
+ sleep 2
+ $0 start
+ ;;
+
+ *)
+ echo "Usage: $0 {start|stop|restart|reload|force-reload}" >&2
+ exit 1
+ ;;
+esac
+
+exit 0
diff --git a/misc/mon_ami/mon_ami b/misc/mon_ami/mon_ami
new file mode 100755
index 0000000..a212cfe
--- /dev/null
+++ b/misc/mon_ami/mon_ami
@@ -0,0 +1,10 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+# MonAMI Asterisk Manger Interface Server
+# (c) AMOOMA GmbH 2012
+from mon_ami_main import main
+from sys import exit
+
+if (__name__ == "__main__"):
+ result = main()
+ exit(result)
diff --git a/misc/mon_ami/mon_ami_handler.py b/misc/mon_ami/mon_ami_handler.py
new file mode 100644
index 0000000..59e9225
--- /dev/null
+++ b/misc/mon_ami/mon_ami_handler.py
@@ -0,0 +1,434 @@
+# -*- coding: utf-8 -*-
+# MonAMI Asterisk Manger Interface Server
+# Asterisk AMI Emulator Handler Process
+# (c) AMOOMA GmbH 2012
+
+from threading import Thread
+from log import ldebug, linfo, lwarn, lerror, lcritic
+from time import sleep
+from traceback import format_exc
+from collections import deque
+from urllib import unquote
+from asterisk import AsteriskAMIServer
+from socket import SHUT_RDWR
+from helper import sval
+
+
+class MonAMIHandler(Thread):
+
+ def __init__(self, socket, address, event_socket=None):
+ Thread.__init__(self)
+ self.runthread = True
+ self.socket = socket
+ self.address = address
+ self.event_socket = event_socket
+ self.ami = None
+ self.deregister_at_server = None
+ self.message_pipe = deque()
+ self.channels = {}
+ self.user_password_authentication = None
+ self.account_name = ''
+
+
+ def stop(self):
+ ldebug('thread stop', self)
+ self.ami.stop()
+ self.runthread = False
+
+
+ def shutdown(self):
+ self.deregister_at_server(self)
+ ldebug('closing connection to %s:%d' % self.address)
+ try:
+ self.socket.shutdown(SHUT_RDWR)
+ self.socket.close()
+ ldebug('connection closed ', self)
+ except:
+ ldebug('connection closed by foreign host', self)
+
+ def run(self):
+ ldebug('starting MonAMI handler thread', self)
+
+ # starting asterisk AMI thread
+ self.ami = AsteriskAMIServer(self.socket, self.address, self.message_pipe)
+ self.ami.start()
+ self.ami.send_greeting()
+
+ # register for events
+ self.event_socket.register_client_queue(self.message_pipe, 'CHANNEL_CREATE')
+ self.event_socket.register_client_queue(self.message_pipe, 'CHANNEL_DESTROY')
+ self.event_socket.register_client_queue(self.message_pipe, 'CHANNEL_STATE')
+ self.event_socket.register_client_queue(self.message_pipe, 'CHANNEL_ANSWER')
+ self.event_socket.register_client_queue(self.message_pipe, 'CHANNEL_BRIDGE')
+
+ while self.runthread and self.ami.isAlive():
+ if self.message_pipe:
+ message = self.message_pipe.pop()
+ message_type = sval(message, 'type')
+ if message_type == 'freeswitch_event':
+ self.handle_fs_event(message['body'])
+ elif message_type == 'ami_client_message':
+ self.handle_ami_client_message(message['body'])
+ else:
+ sleep(0.1)
+
+ self.event_socket.deregister_client_queue_all(self.message_pipe)
+
+ ldebug('exiting MonAMI handler thread', self)
+ self.shutdown()
+
+
+ def handle_ami_client_message(self, message):
+
+ if 'Action' in message:
+ action = message['Action'].lower()
+
+ if action == 'login':
+ if 'UserName' in message:
+ self.account_name = message['UserName']
+ if 'Secret' in message and self.user_password_authentication and self.user_password_authentication(self.account_name, message['Secret']):
+ self.ami.send_login_ack()
+ ldebug('AMI connection authenticated - account: %s' % self.account_name, self)
+ else:
+ self.ami.send_login_nack()
+ linfo('AMI authentication failed - account: %s' % sval(message, 'UserName'), self)
+ self.ami.stop()
+ self.stop()
+ elif action == 'logoff':
+ self.ami.send_logout_ack()
+ ldebug('AMI logout', self)
+ self.ami.stop()
+ self.stop()
+ elif action == 'ping':
+ self.ami.send_pong(sval(message, 'ActionID'))
+ elif action == 'status':
+ self.ami.send_status_ack(sval(message, 'ActionID'))
+ elif action == 'command' and sval(message, 'Command') == 'core show version':
+ self.ami.send_asterisk_version(sval(message, 'ActionID'))
+ elif action == 'hangup':
+ account_name, separator, uuid = str(sval(message, 'Channel')).rpartition('-uuid-')
+ if account_name != '':
+ self.event_socket.hangup(uuid)
+ self.ami.send_hangup_ack()
+ elif action == 'originate':
+ self.message_originate(message)
+ elif action == 'extensionstate':
+ self.ami.send_extension_state(sval(message, 'ActionID'), sval(message, 'Exten'), sval(message, 'Context'))
+ else:
+ ldebug('unknown asterisk message received: %s' % message, self)
+ self.ami.send_message_unknown(message['Action'])
+
+
+ def to_unique_channel_name(self, uuid, channel_name):
+
+ # strip anything left of sip_account_name
+ path, separator, contact_part = channel_name.rpartition('/sip:')
+ if path == '':
+ path, separator, contact_part = channel_name.rpartition('/')
+
+ # if failed return name unchanged
+ if path == '':
+ return channel_name
+
+
+ # strip domain part
+ account_name = contact_part.partition('@')[0]
+
+ # if failed return name unchanged
+ if account_name == '':
+ return channel_name
+
+ # create unique channel name
+ return 'SIP/%s-uuid-%s' % (account_name, uuid)
+
+ def message_originate(self, message):
+ destination_number = str(sval(message, 'Exten'))
+ action_id = sval(message, 'ActionID')
+ self.ami.send_originate_ack(action_id)
+ uuid = self.event_socket.originate(self.account_name, destination_number, action_id)
+
+
+ def handle_fs_event(self, event):
+ event_type = event['Event-Name']
+ #ldebug('event type received: %s' % event_type, self)
+
+ event_types = {
+ 'CHANNEL_CREATE': self.event_channel_create,
+ 'CHANNEL_DESTROY': self.event_channel_destroy,
+ 'CHANNEL_STATE': self.event_channel_state,
+ 'CHANNEL_ANSWER': self.event_channel_answer,
+ 'CHANNEL_BRIDGE': self.event_channel_bridge,
+ }
+
+ uuid = event_types[event_type](event)
+
+ if not uuid:
+ return False
+
+ channel = sval(self.channels, uuid);
+
+ if not channel:
+ return False
+
+ o_uuid = channel['o_uuid']
+ o_channel = sval(self.channels, o_uuid);
+
+ if sval(channel, 'origination_action') or sval(o_channel, 'origination_action'):
+ if not sval(channel, 'ami_start') and not sval(o_channel, 'ami_start'):
+ if sval(channel, 'owned') and sval(channel, 'origination_action'):
+ ldebug('sending AMI events for origitate call start (on this channel): %s' % uuid, self)
+ self.ami_send_originate_start(channel)
+ self.channels[uuid]['ami_start'] = True
+ elif sval(o_channel, 'owned') and sval(o_channel, 'origination_action'):
+ ldebug('sending AMI events for origitate call start (on other channel): %s' % uuid, self)
+ self.ami_send_originate_start(o_channel)
+ self.channels[o_uuid]['ami_start'] = True
+ elif o_channel:
+ if sval(channel, 'owned') and sval(channel, 'origination_action'):
+ ldebug('sending AMI events for origitate call progress (on this channel): %s' % uuid, self)
+ self.ami_send_originate_outbound(channel)
+ self.channels[uuid]['origination_action'] = False
+ elif sval(o_channel, 'owned') and sval(o_channel, 'origination_action'):
+ ldebug('sending AMI events for origitate call progress (on other channel): %s' % uuid, self)
+ self.ami_send_originate_outbound(o_channel)
+ self.channels[o_uuid]['origination_action'] = False
+ elif o_channel:
+ if not sval(channel, 'ami_start') and not sval(o_channel, 'ami_start'):
+ if sval(channel, 'owned') and sval(channel, 'direction') == 'inbound':
+ ldebug('sending AMI events for outbound call start (on this channel): %s' % uuid, self)
+ self.ami_send_outbound_start(channel)
+ self.channels[uuid]['ami_start'] = True
+ elif sval(o_channel, 'owned') and sval(channel, 'direction') == 'outbound':
+ ldebug('sending AMI events for outbound call start (on other channel): %s' % uuid, self)
+ self.ami_send_outbound_start(o_channel)
+ self.channels[o_uuid]['ami_start'] = True
+
+ if not sval(channel, 'ami_start')and not sval(o_channel, 'ami_start'):
+ if sval(channel, 'owned') and sval(channel, 'direction') == 'outbound':
+ ldebug('sending AMI events for inbound call start (on this channel): %s' % uuid, self)
+ self.ami_send_inbound_start(channel)
+ self.channels[uuid]['ami_start'] = True
+ elif sval(o_channel, 'owned') and sval(channel, 'direction') == 'inbound':
+ ldebug('sending AMI events for inbound call start (on other channel): %s' % uuid, self)
+ self.ami_send_inbound_start(o_channel)
+ self.channels[o_uuid]['ami_start'] = True
+
+
+ def event_channel_create(self, event):
+ uuid = sval(event, 'Unique-ID')
+ o_uuid = sval(event, 'Other-Leg-Unique-ID')
+
+ if uuid in self.channels:
+ ldebug('channel already listed: %s' % uuid, self)
+ return false
+
+ channel_name = self.to_unique_channel_name(uuid, unquote(str(sval(event, 'Channel-Name'))))
+ o_channel_name = self.to_unique_channel_name(o_uuid, unquote(str(sval(event, 'Other-Leg-Channel-Name'))))
+
+ if self.account_name in channel_name:
+ channel_owned = True
+ else:
+ channel_owned = False
+
+ if self.account_name in o_channel_name:
+ channel_related = True
+ else:
+ channel_related = False
+
+ if not channel_owned and not channel_related:
+ ldebug('channel neither owned nor reladed to account: %s' % uuid, self)
+ return False
+
+ channel = {
+ 'uuid': uuid,
+ 'name': channel_name,
+ 'direction': sval(event, 'Call-Direction'),
+ 'channel_state': sval(event, 'Channel-State'),
+ 'call_state': sval(event, 'Channel-Call-State'),
+ 'answer_state': sval(event, 'Answer-State'),
+ 'owned': channel_owned,
+ 'related': channel_related,
+ 'caller_id_name': unquote(str(sval(event, 'Caller-Caller-ID-Name'))),
+ 'caller_id_number': unquote(str(sval(event, 'Caller-Caller-ID-Number'))),
+ 'callee_id_name': unquote(str(sval(event, 'Caller-Callee-ID-Name'))),
+ 'callee_id_number': unquote(str(sval(event, 'Caller-Callee-ID-Number'))),
+ 'destination_number': str(sval(event, 'Caller-Destination-Number')),
+ 'origination_action': sval(event, 'variable_origination_action'),
+ 'o_uuid': o_uuid,
+ 'o_name': o_channel_name,
+ }
+
+ if channel['answer_state'] == 'ringing':
+ if channel['direction'] == 'inbound':
+ asterisk_channel_state = 4
+ else:
+ asterisk_channel_state = 5
+ else:
+ asterisk_channel_state = 0
+
+ if not o_uuid:
+ ldebug('one legged call, channel: %s' % uuid, self)
+ elif o_uuid not in self.channels:
+ o_channel = {
+ 'uuid': o_uuid,
+ 'name': o_channel_name,
+ 'direction': sval(event, 'Other-Leg-Direction'),
+ 'channel_state': sval(event, 'Channel-State'),
+ 'call_state': sval(event, 'Channel-Call-State'),
+ 'answer_state': sval(event, 'Answer-State'),
+ 'owned': channel_related,
+ 'related': channel_owned,
+ 'caller_id_name': unquote(str(sval(event, 'Caller-Caller-ID-Name'))),
+ 'caller_id_number': unquote(str(sval(event, 'Caller-Caller-ID-Number'))),
+ 'callee_id_name': unquote(str(sval(event, 'Caller-Callee-ID-Name'))),
+ 'callee_id_number': unquote(str(sval(event, 'Caller-Callee-ID-Number'))),
+ 'destination_number': str(sval(event, 'Other-Leg-Destination-Number')),
+ 'o_uuid': uuid,
+ 'o_name': channel_name,
+ }
+
+ if o_channel['answer_state'] == 'ringing':
+ if o_channel['direction'] == 'inbound':
+ asterisk_o_channel_state = 4
+ else:
+ asterisk_o_channel_state = 5
+ else:
+ asterisk_o_channel_state = 0
+
+ ldebug('create channel list entry for related channel: %s, name: %s' % (o_uuid, o_channel_name), self)
+ self.channels[o_uuid] = o_channel
+ else:
+ ldebug('updating channel: %s, name: %s, o_uuid: %s, o_name %s' % (o_uuid, o_channel_name, uuid, channel_name), self)
+ self.channels[o_uuid]['o_uuid'] = uuid
+ self.channels[o_uuid]['o_name'] = channel_name
+ o_channel = self.channels[o_uuid]
+
+ if channel_owned:
+ ldebug('create channel list entry for own channel: %s, name: %s' % (uuid, channel_name), self)
+ elif channel_related:
+ ldebug('create channel list entry for related channel: %s, name: %s' % (uuid, channel_name), self)
+
+ self.channels[uuid] = channel
+
+ return uuid
+
+
+ def event_channel_destroy(self, event):
+ uuid = sval(event, 'Unique-ID')
+ hangup_cause_code = int(sval(event, 'variable_hangup_cause_q850'))
+ channel = sval(self.channels, uuid)
+
+ if channel:
+ channel['hangup_cause_code'] = hangup_cause_code
+ if sval(channel, 'ami_start'):
+ self.ami_send_outbound_end(channel)
+ del self.channels[uuid]
+ ldebug('channel removed from list: %s, cause %d' % (uuid, hangup_cause_code), self)
+
+ return uuid
+
+
+ def event_channel_state(self, event):
+ uuid = sval(event, 'Unique-ID')
+ channel_state = sval(event, 'Channel-State')
+ call_state = sval(event, 'Channel-Call-State')
+ answer_state = sval(event, 'Answer-State')
+
+ if sval(self.channels, uuid) and False:
+ ldebug('updating channel state - channel: %s, channel_state: %s, call_state %s, answer_state: %s' % (uuid, channel_state, call_state, answer_state), self)
+ self.channels[uuid]['channel_state'] = channel_state
+ self.channels[uuid]['call_state'] = call_state
+ self.channels[uuid]['answer_state'] = answer_state
+
+ return uuid
+
+
+ def event_channel_answer(self, event):
+ uuid = sval(event, 'Unique-ID')
+ o_uuid = sval(event, 'Other-Leg-Unique-ID')
+ channel = sval(self.channels, uuid)
+ if not o_uuid:
+ o_uuid = sval(channel, 'o_uuid')
+ o_channel = sval(self.channels, o_uuid)
+ origination_action = sval(channel, 'origination_action')
+
+ if channel:
+ channel_state = sval(event, 'Channel-State')
+ call_state = sval(event, 'Channel-Call-State')
+ answer_state = sval(event, 'Answer-State')
+ ldebug('channel answered - channel: %s, owned: %s, channel_state: %s, call_state %s, answer_state: %s, other leg: %s' % (uuid, sval(channel, 'owned'), channel_state, call_state, answer_state, o_uuid), self)
+ self.ami.send_event_newstate(uuid, sval(channel, 'name'), 6, sval(channel, 'caller_id_number'), sval(channel, 'caller_id_name'))
+
+ self.channels[uuid]['channel_state'] = channel_state
+ self.channels[uuid]['call_state'] = call_state
+ self.channels[uuid]['answer_state'] = answer_state
+
+ if sval(channel, 'origination_action'):
+ if sval(channel, 'owned'):
+ ldebug('sending AMI originate response - success: %s' % uuid, self)
+ self.ami.send_event_originate_response(sval(channel, 'uuid'), sval(channel, 'name'), sval(channel, 'caller_id_number'), sval(channel, 'caller_id_name'), '101', sval(channel, 'origination_action'), 4)
+ elif not o_uuid:
+ ldebug('sending AMI events for outbound call start on one legged call (this channel): %s' % uuid, self)
+ self.ami_send_outbound_start(channel)
+ self.ami.send_event_bridge(uuid, sval(channel, 'name'), sval(channel, 'caller_id_number'), o_uuid, sval(o_channel, 'name'), sval(o_channel, 'caller_id_number'))
+
+ self.channels[uuid]['ami_start'] = True
+
+ return uuid
+
+ return False
+
+
+ def event_channel_bridge(self, event):
+ uuid = sval(event, 'Unique-ID')
+ o_uuid = sval(event, 'Other-Leg-Unique-ID')
+
+ ldebug('bridge channel: %s to %s' % (uuid, o_uuid), self)
+ channel = sval(self.channels, uuid)
+ o_channel = sval(self.channels, o_uuid)
+
+ if sval(channel, 'owned') or sval(o_channel, 'owned'):
+ ldebug('sending AMI bridge response: %s -> %s' % (uuid, o_uuid), self)
+ self.ami.send_event_bridge(uuid, sval(channel, 'name'), sval(channel, 'caller_id_number'), o_uuid, sval(o_channel, 'name'), sval(o_channel, 'caller_id_number'))
+
+
+ def ami_send_outbound_start(self, channel):
+ self.ami.send_event_newchannel(sval(channel, 'uuid'), sval(channel, 'name'), 0, sval(channel, 'caller_id_number'), sval(channel, 'caller_id_name'), sval(channel, 'destination_number'))
+ self.ami.send_event_newstate(sval(channel, 'uuid'), sval(channel, 'name'), 4, sval(channel, 'caller_id_number'), sval(channel, 'caller_id_name'))
+ self.ami.send_event_newchannel(sval(channel, 'o_uuid'), sval(channel, 'o_name'), 0, '', '', '')
+ self.ami.send_event_dial_begin(sval(channel, 'uuid'), sval(channel, 'name'), sval(channel, 'caller_id_number'), sval(channel, 'caller_id_name'), sval(channel, 'o_name'), sval(channel, 'o_uuid'), sval(channel, 'destination_number'))
+ self.ami.send_event_newcallerid(sval(channel, 'o_uuid'), sval(channel, 'o_name'), sval(channel, 'destination_number'), '', 0)
+ self.ami.send_event_newstate(sval(channel, 'o_uuid'), sval(channel, 'o_name'), 5, sval(channel, 'destination_number'), '')
+
+
+ def ami_send_outbound_end(self, channel):
+ self.ami.send_event_hangup(sval(channel, 'o_uuid'), sval(channel, 'o_name'), sval(channel, 'destination_number'), '', sval(channel, 'hangup_cause_code'))
+ self.ami.send_event_dial_end(sval(channel, 'uuid'), sval(channel, 'name'))
+ self.ami.send_event_hangup(sval(channel, 'uuid'), sval(channel, 'name'), sval(channel, 'caller_id_number'), sval(channel, 'caller_id_name'), sval(channel, 'hangup_cause_code'))
+
+ if sval(channel, 'origination_action'):
+ self.ami.send_event_originate_response(sval(channel, 'uuid'), sval(channel, 'name'), sval(channel, 'caller_id_number'), sval(channel, 'caller_id_name'), sval(channel, 'destination_number'), sval(channel, 'origination_action'), 1)
+
+
+ def ami_send_inbound_start(self, channel):
+ self.ami.send_event_newchannel(sval(channel, 'o_uuid'), sval(channel, 'o_name'), 0, sval(channel, 'caller_id_number'), sval(channel, 'caller_id_name'), sval(channel, 'callee_id_number'))
+ self.ami.send_event_newstate(sval(channel, 'o_uuid'), sval(channel, 'o_name'), 4, sval(channel, 'caller_id_number'), sval(channel, 'caller_id_name'))
+ self.ami.send_event_newchannel(sval(channel, 'uuid'), sval(channel, 'name'), 0, '', '', '')
+ self.ami.send_event_dial_begin(sval(channel, 'o_uuid'), sval(channel, 'o_name'), sval(channel, 'caller_id_number'), sval(channel, 'caller_id_name'), sval(channel, 'name'), sval(channel, 'uuid'), sval(channel, 'destination_number'))
+ self.ami.send_event_newstate(sval(channel, 'uuid'), sval(channel, 'name'), 5, sval(channel, 'caller_id_number'), sval(channel, 'caller_id_name'))
+ self.ami.send_event_newcallerid(sval(channel, 'uuid'), sval(channel, 'name'), sval(channel, 'destination_number'), '', 0)
+
+
+ def ami_send_originate_start(self, channel):
+ self.ami.send_event_newchannel(sval(channel, 'uuid'), sval(channel, 'name'), 0, '', '', '')
+ self.ami.send_event_newcallerid(sval(channel, 'uuid'), sval(channel, 'name'), sval(channel, 'caller_id_number'), sval(channel, 'caller_id_name'), 0)
+ self.ami.send_event_newaccountcode(sval(channel, 'uuid'), sval(channel, 'name'))
+ self.ami.send_event_newcallerid(sval(channel, 'uuid'), sval(channel, 'name'), sval(channel, 'caller_id_number'), sval(channel, 'caller_id_name'), 0)
+ self.ami.send_event_newstate(sval(channel, 'uuid'), sval(channel, 'name'), 5, sval(channel, 'caller_id_number'), sval(channel, 'caller_id_name'))
+
+
+ def ami_send_originate_outbound(self, channel):
+ self.ami.send_event_newchannel(sval(channel, 'o_uuid'), sval(channel, 'o_name'), 0, '', '', '')
+ self.ami.send_event_dial_begin(sval(channel, 'uuid'), sval(channel, 'name'), sval(channel, 'caller_id_number'), sval(channel, 'caller_id_name'), sval(channel, 'o_name'), sval(channel, 'o_uuid'), sval(channel, 'destination_number'))
+ self.ami.send_event_newcallerid(sval(channel, 'o_uuid'), sval(channel, 'o_name'), sval(channel, 'destination_number'), '', 0)
+ self.ami.send_event_newstate(sval(channel, 'o_uuid'), sval(channel, 'o_name'), 5, sval(channel, 'destination_number'), '')
diff --git a/misc/mon_ami/mon_ami_main.py b/misc/mon_ami/mon_ami_main.py
new file mode 100644
index 0000000..13dd4bb
--- /dev/null
+++ b/misc/mon_ami/mon_ami_main.py
@@ -0,0 +1,117 @@
+# -*- coding: utf-8 -*-
+# MonAMI Asterisk Manger Interface Server
+# Main Programm
+# (c) AMOOMA GmbH 2012
+
+from log import ldebug, linfo, lwarn, lerror, lcritic, setup_log
+from time import sleep
+from signal import signal, SIGHUP, SIGTERM, SIGINT
+from optparse import OptionParser
+from freeswitch import FreeswitchEventSocket
+from mon_ami_server import MonAMIServer
+from sqliter import SQLiteR
+
+def signal_handler(signal_number, frame):
+ global event_socket
+ global mon_ami_server
+
+ ldebug('signal %d received ' % signal_number, frame)
+
+ if (signal_number == SIGTERM):
+ ldebug('shutdown signal (%d) received ' % signal_number, frame)
+ event_socket.stop()
+ mon_ami_server.stop()
+ elif (signal_number == SIGINT):
+ ldebug('interrupt signal (%d) received ' % signal_number, frame)
+ event_socket.stop()
+ mon_ami_server.stop()
+ elif (signal_number == SIGHUP):
+ ldebug('hangup signal (%d) received - ignore' % signal_number, frame)
+
+def user_password_authentication(user_name, password):
+ global configuration_options
+
+ if configuration_options.user_ignore_name and configuration_options.user_ignore_password:
+ ldebug('user-password authentication credentials provided but ignored - user: %s, password: %s' % (user_name, '*' * len(str(password))))
+ return True
+
+ if configuration_options.user_override_name != None and configuration_options.user_override_password != None:
+ if user_name == configuration_options.user_override_name and password == configuration_options.user_override_password:
+ return True
+ return False
+
+ db = SQLiteR(configuration_options.user_db_name)
+ if not db.connect():
+ lerror('cound not connect to user database "%s"' % configuration_options.user_db_name)
+ return False
+
+ user = db.find(configuration_options.user_db_table, {configuration_options.user_db_name_row: user_name, configuration_options.user_db_password_row: password})
+ db.disconnect()
+
+ if user:
+ ldebug('user-password authentication accepted - user: %s, password: %s' % (user_name, '*' * len(str(password))))
+ return True
+
+ linfo('user-password authentication failed - user: %s, password: %s' % (user_name, '*' * len(str(password))))
+ return False
+
+def main():
+ global event_socket
+ global mon_ami_server
+ global configuration_options
+
+ option_parser = OptionParser()
+
+ # Log options
+ option_parser.add_option("--log-file", action="store", type="string", dest="log_file", default=None)
+ option_parser.add_option("--log-level", action="store", type="int", dest="log_level", default=5)
+
+ # FreeSWITCH event_socket
+ option_parser.add_option("--freeswitch-address", action="store", type="string", dest="freeswitch_address", default='127.0.0.1')
+ option_parser.add_option("--freeswitch-port", action="store", type="int", dest="freeswitch_port", default=8021)
+ option_parser.add_option("--freeswitch-password", action="store", type="string", dest="freeswitch_password", default='ClueCon')
+
+ # Asterisk Manager Interface
+ option_parser.add_option("-a", "--address", "--ami-address", action="store", type="string", dest="ami_address", default='0.0.0.0')
+ option_parser.add_option("-p", "--port", "--ami-port", action="store", type="int", dest="ami_port", default=5038)
+
+ # User database
+ option_parser.add_option("--user-db-name", action="store", type="string", dest="user_db_name", default='/opt/GS5/db/development.sqlite3')
+ option_parser.add_option("--user-db-table", action="store", type="string", dest="user_db_table", default='sip_accounts')
+ option_parser.add_option("--user-db-name-row", action="store", type="string", dest="user_db_name_row", default='auth_name')
+ option_parser.add_option("--user-db-password-row", action="store", type="string", dest="user_db_password_row", default='password')
+
+ # Define common User/Password options
+ option_parser.add_option("--user-override-name", action="store", type="string", dest="user_override_name", default=None)
+ option_parser.add_option("--user-override-password", action="store", type="string", dest="user_override_password", default=None)
+ option_parser.add_option("--user-ignore-name", action="store_true", dest="user_ignore_name", default=False)
+ option_parser.add_option("--user-ignore-password", action="store_true", dest="user_ignore_password", default=False)
+
+ (configuration_options, args) = option_parser.parse_args()
+
+ setup_log(configuration_options.log_file, configuration_options.log_level)
+ ldebug('starting MonAMI main process')
+
+ # Catch signals
+ signal(SIGHUP, signal_handler)
+ signal(SIGTERM, signal_handler)
+ signal(SIGINT, signal_handler)
+
+ # Starting FreeSWITCH event_socket thread
+ event_socket = FreeswitchEventSocket(configuration_options.freeswitch_address, configuration_options.freeswitch_port, configuration_options.freeswitch_password)
+ event_socket.start()
+
+ if event_socket.isAlive():
+ # Starting Asterisk manager thread
+ mon_ami_server = MonAMIServer(configuration_options.ami_address, configuration_options.ami_port, event_socket)
+ mon_ami_server.user_password_authentication = user_password_authentication
+ mon_ami_server.start()
+
+ while mon_ami_server.isAlive():
+ sleep(1)
+
+ if event_socket.isAlive():
+ ldebug('killing event_socket thread')
+ event_socket.stop()
+
+ ldebug('exiting MonAMI main process')
diff --git a/misc/mon_ami/mon_ami_server.py b/misc/mon_ami/mon_ami_server.py
new file mode 100644
index 0000000..68e72c8
--- /dev/null
+++ b/misc/mon_ami/mon_ami_server.py
@@ -0,0 +1,85 @@
+# -*- coding: utf-8 -*-
+# MonAMI Asterisk Manger Interface Server
+# Asterisk AMI Emulator server thread
+# (c) AMOOMA GmbH 2012
+
+from threading import Thread
+from log import ldebug, linfo, lwarn, lerror, lcritic
+from time import sleep
+from traceback import format_exc
+from tcp_server import TCPServer
+from mon_ami_handler import MonAMIHandler
+import socket
+
+class MonAMIServer(Thread):
+
+ def __init__(self, address=None, port=None, event_socket=None):
+ Thread.__init__(self)
+ self.runthread = True
+ self.port = port
+ self.address = address
+ self.event_socket = event_socket
+ self.handler_threads = {}
+ self.user_password_authentication = None
+
+ def stop(self):
+ ldebug('thread stop', self)
+ ldebug('client connections: %s' % len(self.handler_threads), self)
+ for thread_id, handler_thread in self.handler_threads.items():
+ if handler_thread.isAlive():
+ handler_thread.stop()
+ self.runthread = False
+
+ def register_handler_thread(self, handler_thread):
+ if handler_thread.isAlive():
+ ldebug('registering handler thread %d ' % id(handler_thread), self)
+ self.handler_threads[id(handler_thread)] = handler_thread
+ else:
+ lwarn('handler thread passed away: %d' % id(handler_thread), self)
+
+
+ def deregister_handler_thread(self, handler_thread):
+ if id(handler_thread) in self.handler_threads:
+ ldebug('deregistering handler thread %d ' % id(handler_thread), self)
+ del self.handler_threads[id(handler_thread)]
+ else:
+ lwarn('handler thread %d not registered' % id(handler_thread), self)
+
+
+ def run(self):
+ ldebug('starting MonAMI server thread', self)
+ serversocket = TCPServer(self.address, self.port).listen()
+ #serversocket.setblocking(0)
+
+ if not serversocket:
+ ldebug('server socket could not be bound', self)
+ return 1
+
+ while self.runthread:
+ try:
+ client_socket, client_address = serversocket.accept()
+ except socket.timeout as exception:
+ # Socket timeout occured
+ continue
+ except socket.error as exception:
+ lerror('socket error (%s): %s - ' % (exception, format_exc()), self)
+ sleep(1)
+ continue
+ except:
+ lerror('general error: %s - ' % format_exc(), self)
+ sleep(1)
+ continue
+
+ ldebug('connected to %s:%d' % client_address, self)
+
+ client_thread = MonAMIHandler(client_socket, client_address, self.event_socket)
+ client_thread.deregister_at_server = self.deregister_handler_thread
+ client_thread.user_password_authentication = self.user_password_authentication
+ client_thread.start()
+ if client_thread.isAlive():
+ self.register_handler_thread(client_thread)
+
+ ldebug('registered handler threads: %d' % len(self.handler_threads), self)
+
+ ldebug('exiting MonAMI server thread', self)
+
diff --git a/misc/mon_ami/sqliter.py b/misc/mon_ami/sqliter.py
new file mode 100644
index 0000000..5b03729
--- /dev/null
+++ b/misc/mon_ami/sqliter.py
@@ -0,0 +1,132 @@
+# -*- coding: utf-8 -*-
+# SQLite library
+
+import sqlite3
+
+class SQLiteR():
+
+ def __init__(self, database = None):
+ self.db_name = database
+ if (self.db_name == None):
+ self.db_name = ':memory:'
+ self.db_conn = None
+ self.db_cursor = None
+
+ def record_factory(self, cursor, row):
+ record = dict()
+ for index, column in enumerate(cursor.description):
+ record[column[0]] = row[index]
+ return record
+
+ def connect(self, isolation_level = None):
+ try:
+ self.db_conn = sqlite3.connect(self.db_name)
+ self.db_conn.row_factory = self.record_factory
+ self.db_cursor = self.db_conn.cursor()
+ except:
+ return False
+
+ self.db_conn.isolation_level = isolation_level
+ return True
+
+ def disconnect(self):
+ try:
+ self.db_nonn.close()
+ except:
+ return False
+ return True
+
+ def execute(self, query, parameters = []):
+ try:
+ return self.db_cursor.execute(query, parameters)
+ except:
+ return False
+
+ def fetch_row(self):
+ return self.db_cursor.fetchone()
+
+ def fetch_rows(self):
+ return self.db_cursor.fetchall()
+
+ def execute_get_rows(self, query, parameters = []):
+ if (self.execute(query, parameters)):
+ return self.fetch_rows()
+ else:
+ return False
+
+ def execute_get_row(self, query, parameters = []):
+ query = "%s LIMIT 1" % query
+ if (self.execute(query, parameters)):
+ return self.fetch_row()
+ else:
+ return False
+
+ def execute_get_value(self, query, parameters = []):
+ row = self.execute_get_row(query, parameters)
+ if (row):
+ return row[0]
+ else:
+ return row
+
+ def create_table(self, table, structure, primary_key = None):
+ columns = list()
+ for row in structure:
+ key, value = row.items()[0]
+ sql_type = "VARCHAR(255)"
+ sql_key = ''
+ if (key == primary_key):
+ sql_key = 'PRIMARY KEY'
+ type_r = value.split(':', 1)
+ type_n = type_r[0]
+ if (type_n == 'integer'):
+ sql_type = 'INTEGER'
+ elif (type_n == 'string'):
+ try:
+ sql_type = "VARCHAR(%s)" % type_r[1]
+ except IndexError, e:
+ sql_type = "VARCHAR(255)"
+
+ columns.append('"%s" %s %s' % (key, sql_type, sql_key))
+
+ query = 'CREATE TABLE "%s" (%s)' % (table, ', '.join(columns))
+ return self.execute(query)
+
+ def save(self, table, row):
+ keys = row.keys()
+ query = 'INSERT OR REPLACE INTO "%s" (%s) VALUES (:%s)' % (table, ', '.join(keys), ', :'.join(keys))
+
+ return self.execute(query, row)
+
+ def find_sql(self, table, rows = None):
+ values = list()
+ if (rows):
+ if (type(rows) == type(list())):
+ rows_list = rows
+ else:
+ rows_list = list()
+ rows_list.append(rows)
+
+ query_parts = list()
+
+ for row in rows_list:
+ statements = list()
+ for key, value in row.items():
+ if (value == None):
+ statements.append("\"%s\" IS ?" % (key))
+ else:
+ statements.append("\"%s\" = ?" % (key))
+ values.append(value)
+ query_parts.append('(%s)' % ' AND '.join(statements))
+
+ query = 'SELECT * FROM "%s" WHERE %s' % (table, ' OR '.join(query_parts))
+ else:
+ query = 'SELECT * FROM "%s"' % table
+ return query, values
+
+ def find(self, table, row = None):
+ query, value = self.find_sql(table, row)
+ return self.execute_get_row(query, value)
+
+ def findall(self, table, row = None):
+ query, values = self.find_sql(table, row)
+ return self.execute_get_rows(query, values)
diff --git a/misc/mon_ami/tcp_server.py b/misc/mon_ami/tcp_server.py
new file mode 100644
index 0000000..5536282
--- /dev/null
+++ b/misc/mon_ami/tcp_server.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+# MonAMI Asterisk Manger Interface Server
+# TCP Server
+# (c) AMOOMA GmbH 2012
+
+import socket
+from traceback import format_exc
+from log import ldebug, linfo, lwarn, lerror, lcritic
+
+class TCPServer():
+
+ def __init__(self, address=None, port=None, timeout=1):
+ self.SOCKET_BACKLOG = 5
+ self.port = port
+ self.address = address
+ self.socket_timeout = timeout
+
+ def listen(self):
+ tcpsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ tcpsocket.setsockopt( socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 )
+
+ ldebug('binding server to %s:%d, timeout: %d' % (self.address, self.port, self.socket_timeout), self)
+
+ try:
+ tcpsocket.bind((self.address, self.port))
+ except ValueError as exception:
+ lerror('server socket address error: %s - %s' % (exception, format_exc()), self)
+ return False
+ except socket.error as exception:
+ lerror('server socket error (%d): %s - %s' % (exception[0], exception[1], format_exc()), self)
+ return False
+ except:
+ lerror('general server socket error: %s' % format_exc(), self)
+ return False
+
+ tcpsocket.listen(self.SOCKET_BACKLOG)
+ tcpsocket.settimeout(self.socket_timeout)
+
+ return tcpsocket
diff --git a/misc/nginx/nginx.conf b/misc/nginx/nginx.conf
new file mode 100644
index 0000000..c1c1a39
--- /dev/null
+++ b/misc/nginx/nginx.conf
@@ -0,0 +1,81 @@
+
+user www-data;
+worker_processes 1;
+
+#error_log logs/error.log;
+#error_log logs/error.log notice;
+#error_log logs/error.log info;
+
+#pid logs/nginx.pid;
+
+
+events {
+ worker_connections 1024;
+}
+
+
+http {
+ passenger_root /usr/local/rvm/gems/ruby-1.9.2-p290/gems/passenger-3.0.11;
+ passenger_ruby /usr/local/rvm/wrappers/ruby-1.9.2-p290/ruby;
+
+ include mime.types;
+ default_type application/octet-stream;
+
+ #log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ # '$status $body_bytes_sent "$http_referer" '
+ # '"$http_user_agent" "$http_x_forwarded_for"';
+
+ #access_log logs/access.log main;
+
+ sendfile on;
+ #tcp_nopush on;
+
+ #keepalive_timeout 0;
+ keepalive_timeout 65;
+
+ #gzip on;
+
+ server {
+ listen 80;
+ server_name localhost;
+ root /opt/GS5/public;
+ passenger_enabled on;
+ }
+
+ # another virtual host using mix of IP-, name-, and port-based configuration
+ #
+ #server {
+ # listen 8000;
+ # listen somename:8080;
+ # server_name somename alias another.alias;
+
+ # location / {
+ # root html;
+ # index index.html index.htm;
+ # }
+ #}
+
+
+ # HTTPS server
+ #
+ #server {
+ # listen 443;
+ # server_name localhost;
+
+ # ssl on;
+ # ssl_certificate cert.pem;
+ # ssl_certificate_key cert.key;
+
+ # ssl_session_timeout 5m;
+
+ # ssl_protocols SSLv2 SSLv3 TLSv1;
+ # ssl_ciphers HIGH:!aNULL:!MD5;
+ # ssl_prefer_server_ciphers on;
+
+ # location / {
+ # root html;
+ # index index.html index.htm;
+ # }
+ #}
+
+}