Телефонная книга Freeswitch

в 12:17, , рубрики: asterisk, freeswitch, ip-pbx, ip-телефония, voip, Блог компании BUSINESS INFINITY GROUP, Разработка систем связи, системное программирование

Телефонная книга Freeswitch - 1

Внесение

Так уж получилось, что Freeswitch – моя маленькая слабость. Да, сейчас в мире более распространен Asterisk, и я его тоже неплохо знаю, но… Чем-то меня привлекает именно Freeswitch. Может набором возможностей, может сверхстабильной работой, может малым потреблением ресурсов и более логичным устройством. А может я просто хипстер от мира IT и желаю быть не как все. Как знать. Мы не психологи, чтобы копаться в потемках человеческой души, поэтому просто примем это как данность и займёмся вещами более практичными. Будем улучшать клиентский сервис. На повестке дня – улучшенная персонализация клиента путём его узнавания.

Тривия

Телефонная книга – вещь предельно утилитарная. Присутствует во всех более-менее приличных надстройках над системами IP-телефонии. Позволяет при входящем звонке опознать клиента по номеру телефона. Конечно, мы не отдел продаж, в CRM всех подряд не пишем, но даже сотруднику клиента, который звонит в техническую поддержку со своими проблемами, приятно, когда его сразу узнают. На уровне приложения телефонная книга обычно подключается как плагин, позволяя использовать в качестве источника данных определённое хранилище информации, чаще всего базу данных. Не избежал подобной участи и Freeswitch. В разное время у нас использовалось два варианта телефонной книги:

  • на основе плагина и базы данных SQLite;
  • на основе самописного REST-коннектора на Lua (используется и поныне).

Я расскажу про реализацию обоих вариантов. У каждого свои преимущества и недостатки.

Телефонная книга на основе БД проще в устройстве и более стандартна. На официальном сайте Freeswitch есть неплохая дока, по её установке и настройке. Но поскольку на сервере телефонии нет веб-интерфейса (я принципиальный противник веб-интерфейсов к телефонным станциям, но это уже совсем другая история, как-нибудь в следующий раз), то настраивать телефонную книгу может ограниченный круг неограниченных лиц, которых не пугают слова база данных, запрос и иже с ними. А то и консоль, как в моём случае. Вы спросите, а почему не MySQL? Потому что в нём не было необходимости. У нас же не вся Россия в телефонной книге, а всего лишь около 500 контактов. Для этого SQLite оказалось за глаза. Второй сложностью стал сам процесс добавления информации. Добавлять надо в несколько таблиц, учитывать связи и не забывать про дубликаты. У меня до сих пор где-то лежит документ по порядку выполнения запросов на добавление и обновление информации в телефонной книге. Преимущества же очевидны – движок базы данных не требует установки и интеграции дополнительного софта и сам по себе прост, как мычание. Вся телефонная книга лежит в одном файле, легко резервируется и сохраняется. Таблицы внутри предельно простые и понимание их структуры не требует особого мыслительного процесса. Достаточно поднять палку потяжелее и стукнуть соседа слева. Пусть он думает, да.

Второй вариант телефонной книги был менее тривиален в реализации. Так как мы сейчас активно пытаемся освоить CMDB iTop, появилась идея скрестить телефонную книгу с ней. Благо, стандартный модуль хранения контактов в CMDB наличествует и соответствует. Стандартными методами это сделать не удалось. На помощь пришла Lua, с ней все получилось чисто и аккуратно. Из преимуществ такого подхода можно отметить наличие удобного интерфейса для добавления и редактирования контактов, возможность сохранять любые данные в CMDB, передавать их в телефонную станцию и использовать при маршрутизации звонка (пока не используется, но планируется). Из недостатков — конечно, задержки передачи по сети, зависимость от внешнего сервиса, необходимость программирования и наладки всего этого хозяйства. Но результат того стоит. И, кстати, я не убеждаю вас применять именно такой вариант, просто на его примере хотел бы показать, как можно организовать взаимодействие с внешними сервисами.

Piece of cake

По старинной IT-шной традиции пойдём от простого к сложному. Начнём с того, что ляжем на диван и займёмся прокрастинацией. Видите, как просто. Но работа – не волк, с ноги не пнёшь, а потому переводим тело в вертикальное положение и перемещаем поближе к консоли телефонной станции…
Модуль телефонной книги в Freeswitch называется mod_cidlookup Описание на старом сайте Описание на новом сайте. Для начала надо определиться, стоит ли модуль у нас. Как знать, вдруг просочился незаметно.

Смотрим наличие модуля в папке модулей.

ls -la /usr/local/freeswitch/mod/ | grep mod_cidlookup

Если никаких файлов не оказалось, то у меня для вас плохие новости. Модуль надо собрать или доустановить. Я предпочитаю собирать Freeswitch самостоятельно, поэтому дерево исходников, готовых для сборки, всегда под рукой. Описание того, как собрать модуль для Freeswitch выходит за рамки этой статьи, потому оставим его там и просто примем, что модуль уже лежит в нужной нам папке. Теперь его надо сконфигурировать и подготовить для него данные. Основной конфиг настройки модуля расположен в файле

/usr/local/freeswitch/conf/autoload_configs/cidlookup.conf.xml
Минимально рабочий конфиг выглядит примерно так.

Рабочий конфиг

<configuration name="cidlookup.conf" description="cidlookup Configuration">
	<settings>
		<!-- Кэшируем запрашиваемые данные. -->
		<param name="cache" value="true"/>
		<!-- Время жизни записи в кэше - сутки. -->
		<param name="cache-expire" value="86400"/>
 
		<!--
			Строка подключения. Особое внимание на 3 (ТРИ, Карл!) слэша в
			начале строки. Всё именно так, иначе работать не будет, я
			проверил. Путь должен быть абсолютным, с относительным база не
			подключалась.
		-->
		<param name="odbc-dsn" value="sqlite:///usr/local/freeswitch/db/phonebook.db"/>
 
		<!--
			Запрос на извлечение данных. Принимает подстановочный параметр с
			номером телефона. Запрос обязательно должен возвращать одну
			строку.
		-->
		<param name="sql" value="
			SELECT (name || (CASE WHEN comment != '' THEN ' (' || comment || ')' ELSE '' END)) AS name
				FROM numbers n JOIN phonebook p ON n.pid = p.id
				WHERE n.number='${caller_id_number}'
				LIMIT 1
		"/>
	</settings>
</configuration>

Обратите внимание на условное выражение в SELECT. Оно позволяет извлекать из базы компанию клиента и из поля комментария его должность. Если же поле пустое, то возвращается только фамилия и имя, как и положено. В связи с тем, что всегда возвращается только одна строка, дубликаты игнорируются, и возвращается самая ранняя запись из телефонной книги. Поэтому перед добавлением данных надо обязательно проверять, что их нет в базе. Связывание в телефонной книге используется для поддержки нескольких номеров телефонов у одного пользователя.

Переходим к созданию базы данных для нашей телефонной книги.

sudo -u freeswitch sqlite3 /usr/local/freeswitch/db/phonebook.db

-- Создаем необходимые таблицы и индексы.
-- Контакты.
CREATE TABLE phonebook (
	id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL UNIQUE,
	name VARCHAR (128) NOT NULL,
	comment VARCHAR (256)
);
CREATE INDEX name ON phonebook (name);
 
-- Номера телефонов.
CREATE TABLE numbers (
	id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
	pid INTEGER REFERENCES phonebook (id) ON DELETE CASCADE NOT NULL,
	NUMBER VARCHAR (32) NOT NULL
);
CREATE INDEX NUMBER ON numbers (NUMBER);
 
-- Добавляем немного данных для тестирования.
INSERT INTO `phonebook` (`name`, `comment`) VALUES ('Иван Иванов', 'Рога и копыта, менеджер');
INSERT INTO `numbers` (`pid`, `number`) VALUES (LAST_INSERT_ROWID(), '79152323245');
INSERT INTO `phonebook` (`name`) VALUES ('Петр Петров');
INSERT INTO `numbers` (`pid`, `number`) VALUES (LAST_INSERT_ROWID(), '74953462323');

Выходим из консоли SQLite.

.exit

Теперь надо загрузить модуль и натравить его на свежесозданную телефонную книгу. Заходим в консоль Freeswitch.

fs_cli

Ищем загруженный модуль телефонной книги.

freeswitch@internal> show modules mod_cidlookup

0 total.

Ага, модуля нет. Пробуем его загрузить.

freeswitch@internal> load mod_cidlookup
+OK Reloading XML
+OK

2016-10-29 14:36:33.890548 [DEBUG] mod_cidlookup.c:122 Connecting to dsn: sqlite:///usr/local/freeswitch/db/phonebook.db
2016-10-29 14:36:33.890548 [INFO] mod_enum.c:880 ENUM Reloaded
2016-10-29 14:36:33.890548 [INFO] switch_time.c:1415 Timezone reloaded 1781 definitions
2016-10-29 14:36:33.890548 [CONSOLE] switch_loadable_module.c:1538 Successfully Loaded [mod_cidlookup]
2016-10-29 14:36:33.890548 [NOTICE] switch_loadable_module.c:292 Adding Application 'cidlookup'
2016-10-29 14:36:33.890548 [NOTICE] switch_loadable_module.c:338 Adding API Function 'cidlookup'
.

Отлично, модуль загрузился. Если возникнут проблемы с подключением к базе, то они появятся в логе. Ищем его повторно среди загруженных модулей.

freeswitch@internal> show modules mod_cidlookup
type,name,ikey,filename
api,cidlookup,mod_cidlookup,/usr/local/freeswitch/mod/mod_cidlookup.so
application,cidlookup,mod_cidlookup,/usr/local/freeswitch/mod/mod_cidlookup.so

2 total.

Теперь необходимо протестировать работоспособность модуля. Это также можно сделать через консоль, используя API-функцию cidlookup. Смотрим ее синтаксис.

freeswitch@internal> show api cidlookup
name,description,syntax,ikey
cidlookup,cidlookup API,cidlookup status|number [skipurl] [skipcitystate] [verbose],mod_cidlookup

1 total.

Припоминаем те данные, которые мы добавляли в телефонную книгу и тестируем работу API.

freeswitch@internal> cidlookup 79152323245
Иван Иванов (Рога и копыта, менеджер)

freeswitch@internal> cidlookup 74953462323
Петр Петров

Как можно видеть, функция возвращает корректные данные контакта из телефонной книги. При этом, если есть комментарий, то он тоже возвращается в скобках. Теперь надо добавить эту функцию в диалплан, чтобы нести свет и радость людям. Так как диалплан у всех устроен по-разному, я покажу только его часть, которая касается телефонной книги и укажу её примерное размещение. У меня она располагается в файле /usr/local/freeswitch/conf/dialplan/public.xml.

<!-- Активируем поиск по номеру телефона. -->
<!-- Присваиваем значение по умолчанию для номера телефона. -->
<extension name="cid_number_cleanup" continue="true">
	<condition field="caller_id_number" expression="^(d+)$">
		<action application="set" data="effective_caller_id_number=$1" inline="true"/>
	</condition>
</extension>
 
<!-- Присваиваем значение по умолчанию для имени контакта. -->
<extension name="cid_name_cleanup" continue="true">
	<condition field="caller_id_name" expression="^(d+)$">
		<action application="set" data="effective_caller_id_name=$1" inline="true"/>
	</condition>
</extension>
 
<!--
	Значения по умолчанию оберегают нас от ситуации, когда поиск номера в телефонной книге невозможен,
	не успевает выполниться, или модуля вообще нет. В этом случае мы просто получим вместо имени контакта
	его номер.
-->
 
<extension name="cid_lookup" continue="true">
	<condition field="${module_exists(mod_cidlookup)}" expression="true"/>
	<condition field="caller_id_name" expression="^(d+)$|^$"/>
	<condition field="caller_id_number" expression="^(d+)$">
		<action application="cidlookup" data="$1"/>
	</condition>
</extension>

Сохраняем файл, выполняем перезагрузку диалплана командой

fs_cli -x reloadxml

Если ошибок нет, то можно проверять работу модуля. Вдоволь наигравшись, добавляем модуль в автозагрузку. Это делается в файле /usr/local/freeswitch/conf/autoload_configs/modules.conf.xml. Ищем там строчку

<load module="mod_cidlookup"/>

и раскомментируем её. Если же она отсутствует, то просто добавляем её в конец файла.

Вот и всё, телефонная книга настроена. Но админы не были бы админами, если бы не хотели постоянно что-то улучшить. И поход по этой дороге приводит к следующей части нашего повествования.

Let's Rock

Взаимодействие Freeswitch и iTop устроено вполне стандартным способом, через REST-интерфейс, описанный на официальном сайте. В чем же сложность? В том, что напрямую с ним взаимодействовать не получится, надо призывать на помощь силу лебедя плагинов. Изначально планировалось использовать mod_curl, но с ним как-то не заладилось. Было принято решение написать свой небольшой скрипт на Lua и вызывать его через mod_lua. Перво-наперво надо подготовить саму телефонную книгу, то есть тем или иным способом загрузить контакты в iTop. На ваш выбор — источники синхронизации, загрузка CSV или наполнение телефонной книги вручную. Так как у нас тут песочница и много контактов нам не надо, то, давайте, создадим одну организацию и внесём в нее несколько контактов. Для внесения изменений в CMDB вы должны обладать соответствующими правами или быть администратором.

  • Открываем iTop, в боковом меню идём по адресу Администрирование данных → Организации и жмём справа кнопку «Новый…».

Телефонная книга Freeswitch - 2

  • Заполняем необходимые поля и жмём кнопку «Создать».

Телефонная книга Freeswitch - 3

  • Если всё сделано правильно, нас перебросит на страницу созданной организации.

Телефонная книга Freeswitch - 4

  • Теперь в меню переходим по адресу Управление конфигурациями → Контакты → Создать контакт. В открывшемся окне выбираем тип контакта «Персона» и нажимаем «Применить».

Телефонная книга Freeswitch - 5

  • Откроется окно добавления контакта. Заполните все необходимые поля и нажмите кнопку «Создать». Обратите внимание, что в стандартной установке iTop нет части полей с дополнительными телефонами, они добавлены мной путем редактирования модели, чтобы решить проблему с несколькими телефонами. Редактирование модели выходит за рамки статьи, подробнее с ним можно ознакомиться тут

.
Телефонная книга Freeswitch - 6

  • Если вас перебросило на страницу контакта, то всё сделано правильно.

Телефонная книга Freeswitch - 7
Похожим образом добавляем ещё несколько контактов. Теперь у нас есть, где искать и можно переходить к настройке телефонной станции. Как я уже говорил, запрашивать будем через REST-интерфейс iTop. Его распечатка и вдумчивое вкуривание вылились в следующий запрос.

{
	"operation": "core/get",
	"class": "Person",
	"key": "SELECT Person AS P WHERE P.phone = '79101001122' OR P.add_phone = '79101001122' OR P.add_phone_2 = '79101001122' OR P.mobile_phone = '79101001122' OR P.add_mobile_phone = '79101001122'",
	"output_fields": "friendlyname,org_id_friendlyname,function"
}

Попробуем выполнить его при помощи Curl.

curl -XPOST 'https://<itop_address>/webservices/rest.php?version=1.0' -d 'auth_user=admin&auth_pwd=password&json_data=%7B%22operation%22%3A%22core%2Fget%22%2C%22class%22%3A%22Person%22%2C%22key%22%3A%22SELECT%20Person%20AS%20P%20WHERE%20P.phone%20%3D%20%2779101001122%27%20OR%20P.add_phone%20%3D%20%2779101001122%27%20OR%20P.add_phone_2%20%3D%20%2779101001122%27%20OR%20P.mobile_phone%20%3D%20%2779101001122%27%20OR%20P.add_mobile_phone%20%3D%20%2779101001122%27%22%2C%22output_fields%22%3A%22friendlyname%2Corg_id_friendlyname%2Cfunction%22%7D'

Как можно заметить, запрос требует аутентификации. Пользователь в iTop должен существовать и обладать необходимыми правами для выполнения запросов.

В ответ получаем найденного пользователя:

{
	"objects": {
		"Person::486": {
			"code": 0,
			"message": "",
			"class": "Person",
			"key": "486",
			"fields": {
				"friendlyname": "u0412u0430u0441u0438u043bu0438u0439 u041fu0443u043fu043au0435u0432u0438u0447",
				"org_id_friendlyname": "u0420u043eu0433u0430 u0438 u043au043eu043fu044bu0442u0430",
				"function": "u0421u0443u043fu0435u0440u0445u043eu043cu044fu043a"
			}
		}
	},
	"code": 0,
	"message": "Found: 1"
}

В более читабельном варианте

{
	"objects": {
		"Person::486": {
			"code": 0,
			"message": "",
			"class": "Person",
			"key": "486",
			"fields": {
				"friendlyname": "Василий Пупкевич",
				"org_id_friendlyname": "Рога и копыта",
				"function": "Суперхомяк"
			}
		}
	},
	"code": 0,
	"message": "Found: 1"
}

Итак, данные принимаются и передаются, пользователи ищутся. Самое время расчехлить любимую IDE и написать что-нибудь вдохновенное. Создаём отдельную папку для скриптов Freeswitch.

mkdir /usr/local/freeswitch/scripts

В этой папке создаём подпапку lib, в которой будем хранить общие библиотеки.

mkdir /usr/local/freeswitch/scripts/lib

Нам понадобятся стандартные библиотеки для работы c сокетами и SSL.

sudo apt-get install lua-socket lua-sec

Также пригодится библиотека для кодирования и декодирования JSON. Я остановил свой выбор на библиотеке http://regex.info/blog/lua/json. Скачиваем её и размещаем в соответствующей папке.


wget http://regex.info/code/JSON.lua -O lib/json.lua

И вот теперь всё готово для пришествия нашего кода в этот мир. В папке скриптов создаём файл cidlookup.lua и твёрдой клавиатурой вносим туда следующий код:

/usr/local/freeswitch/scripts/cidlookup.lua

-- Параметры соединения с iTop.
local itop_addr = 'https://<itop_address:port>';
local itop_user = 'admin';
local itop_pass = 'password';
 
-- Загружаем библиотеки.
local json = (loadfile '/usr/local/freeswitch/scripts/lib/json.lua')();
 
-- Принимаем из аргументов номер.
--[[
	Обратите внимание на две следующие строки. Они указывают на различия в принимаемых аргументах при вызове
	из командной строки и из Freeswitch. При тестировании из командной строки надо раскомментировать верхнюю
	строку и закомментировать нижнюю. И наоборот. Да, я знаю, можно было сделать изящнее и универсальнее. Но
	я не так хорошо знаю Lua, чтобы сделать это хорошо. Может когда-нибудь.
]]--
-- local phone = arg[1];
local phone = argv[1];
 
-- Формируем команду.
local command = {
	operation = 'core/get',
	class = 'Person',
	key = 'SELECT Person AS P WHERE P.phone = "' .. phone .. '" OR P.add_phone = "' .. phone .. '" OR P.add_phone_2 = "' .. phone .. '" OR P.mobile_phone = "' .. phone .. '" OR P.add_mobile_phone = "' .. phone .. '"',
	output_fields = 'friendlyname,org_id_friendlyname,function'
};
 
-- Подключаемся к iTop.
local http = require 'socket.http';
local https = require 'ssl.https';
local ltn12 = require 'ltn12';
 
local request = 'auth_user=' .. itop_user .. '&auth_pwd=' .. itop_pass .. '&json_data=' .. json:encode(command);
local respbody = {};
-- Таймаут требуется, чтобы не вызывать зависание звонка, в случае если iTop по какой-то причине не отвечает.
-- Не ответил за три секунды - всё, отдаем номер.
http.TIMEOUT = 3; 
local body, code, headers, status = https.request {
	protocol = 'tlsv1',
	method = 'POST',
        url = 'https://' .. itop_addr .. '/webservices/rest.php?version=1.0',
	source = ltn12.source.string(request),
        headers = 
                {
                        ["Accept"] = "*/*",
                        ["Accept-Encoding"] = "gzip, deflate",
                        ["Accept-Language"] = "en-us",
                        ["Content-Type"] = "application/x-www-form-urlencoded",
                        ["content-length"] = string.len(request)
                },
        sink = ltn12.sink.table(respbody)
    };
 
-- Если запрос не сработает, нам вернется телефон звонящего.
caller_id = phone;
-- Декодируем JSON.
local response = json:decode(tostring(table.concat(respbody)));
-- Проверяем, что есть какой-то результат.
if(not((response == nil) or (response["objects"] == nil))) then
	local index = next(response["objects"]);
	local contact = response.objects[index]["fields"];
-- Формируем выходную строку.
	caller_id = contact.friendlyname .. "(".. contact.org_id_friendlyname .. (not(contact["function"] == "") and ", " .. contact["function"] or "") .. ")";
end
 
--[[
	Здесь также присутствует отличие при выполнении скрипта из командной строки и из Freeswitch. Для проверки
	работы скрипта из командной строки комментируем верхнюю строку и раскомментируем нижнюю. И наоборот.
]]--
stream:write(caller_id);
-- io.write(caller_id .. "n");

Настало время протестировать наше творение в бою. Редактируем скрипт для его работы из командной строки и проверяем на работоспособность.

lua cidlookup.lua 79101001122
Василий Пупкевич(Рога и копыта, Суперхомяк)

Отлично, всё работает как надо. Редактируем скрипт, отключая работу из командной строки. Теперь подключаем скрипт к Freeswitch. Сначала надо убедиться, что у нас есть соответствующий модуль. Заходим в консоль Freeswitch.

fs_cli

Проверяем загруженность модуля Lua.

freeswitch@internal> show modules mod_lua
type,name,ikey,filename
api,lua,mod_lua,/usr/local/freeswitch/mod/mod_lua.so
api,luarun,mod_lua,/usr/local/freeswitch/mod/mod_lua.so
application,lua,mod_lua,/usr/local/freeswitch/mod/mod_lua.so
dialplan,LUA,mod_lua,/usr/local/freeswitch/mod/mod_lua.so

4 total.

Если вы видите эти строки, то все в порядке, модуль загружен. Если же их нет, возможно модуль собран, но не запущен. Пытаемся его запустить.

freeswitch@internal> load mod_lua

Если Freeswitch не сможет найти модуля для запуска, то надо будет его собрать и доустановить. Будем считать, что эта проблема была вами успешно решена и модуль загружен. Пробуем выполнить наш скрипт:

freeswitch@internal> lua cidlookup.lua 79101001122
Василий Пупкевич(Рога и копыта, Суперхомяк)

Отлично, как видите, скрипт успешно работает и всё, что надо получает. Осталось добавить его в диалплан. Как и в первой части статьи, я не буду указывать вам, куда его положить, просто приведу часть диалплана, ответственного за его работу.

<!-- Активируем поиск по номеру телефона. -->
<!-- Присваиваем значение по умолчанию для номера телефона. -->
<extension name="cid_number_cleanup" continue="true">
	<condition field="caller_id_number" expression="^(d+)$">
		<action application="set" data="effective_caller_id_number=$1" inline="true"/>
	</condition>
</extension>
 
<!-- Присваиваем значение по умолчанию для имени контакта. -->
<extension name="cid_name_cleanup" continue="true">
	<condition field="caller_id_name" expression="^(d+)$">
		<action application="set" data="effective_caller_id_name=$1" inline="true"/>
	</condition>
</extension>
 
<!--
	Значения по умолчанию оберегают нас от ситуации, когда поиск номера в телефонной книге невозможен,
	не успевает выполниться или модуля вообще нет. В этом случае мы просто получим вместо имени контакта
	его номер.
-->
 
<extension name="cid_lookup" continue="true">
	<condition field="${module_exists(mod_lua)}" expression="true"/>
	<condition field="caller_id_name" expression="^(d+)$|^$"/>
	<condition field="caller_id_number" expression="^(d+)$">
		<action application="set" data="effective_caller_id_name=${lua(cidlookup.lua ${caller_id_name})}"/>
	</condition>
</extension>

Выполняем перезагрузку диалплана командой

fs_cli -x reloadxml

И наслаждаемся результатом.

Вынесение

Да, я знаю, можно улучшать и улучшать. Можно связать с mod_curl и упростить код, можно допилить и оптимизировать скрипт, не спорю с этим. Нет предела совершенству. Но любая дорога начинается с первого шага, и если эта статья кому-то пригодится в его нелегком труде, значит, писалась не зря. =) Засим желаю здравствовать.

P.S. Надеюсь, кому-нибудь пригодится. Буду рад вашим комментариям.

Автор: binfini

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js