Внесение
Так уж получилось, что 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, в боковом меню идём по адресу Администрирование данных → Организации и жмём справа кнопку «Новый…».
- Заполняем необходимые поля и жмём кнопку «Создать».
- Если всё сделано правильно, нас перебросит на страницу созданной организации.
- Теперь в меню переходим по адресу Управление конфигурациями → Контакты → Создать контакт. В открывшемся окне выбираем тип контакта «Персона» и нажимаем «Применить».
- Откроется окно добавления контакта. Заполните все необходимые поля и нажмите кнопку «Создать». Обратите внимание, что в стандартной установке iTop нет части полей с дополнительными телефонами, они добавлены мной путем редактирования модели, чтобы решить проблему с несколькими телефонами. Редактирование модели выходит за рамки статьи, подробнее с ним можно ознакомиться тут
.
- Если вас перебросило на страницу контакта, то всё сделано правильно.
Похожим образом добавляем ещё несколько контактов. Теперь у нас есть, где искать и можно переходить к настройке телефонной станции. Как я уже говорил, запрашивать будем через 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 и твёрдой клавиатурой вносим туда следующий код:
-- Параметры соединения с 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