Модули расширения системы > Примеры модулей интеграции с ELMA365 / Интеграция с IP-телефонией через пользовательский модуль

Интеграция с IP-телефонией через пользовательский модуль

Вы можете настроить интеграцию с провайдерами IP-телефонии, чтобы сотрудники могли совершать и принимать звонки от клиентов. Для этого используйте:

Начало внимание

Функциональные возможности телефонии доступны при активации одного из платных решений CRM, в состав которого входит решение ELMA365 Управление коммуникациями.

Конец внимание

Чтобы настроить интеграцию с помощью пользовательского модуля:

  1. Выполните предварительные настройки, например, проверьте доступ к аккаунту телефонии, установите SIP-софтфон и т. д.
  2. Создайте модуль и в его скриптах пропишите методы Web API, чтобы получить сгенерированный Webhook и токен для обмена данными с провайдером телефонии.
  3. Соедините модуль с провайдером — в настройках на стороне провайдера укажите токен и Webhook, а в модуль скопируйте ключ авторизации и URL телефонной станции.

Чтобы пользователи могли обработать звонок в его карточке, например принять или переназначить вызов и т. д., добавьте в скрипты модуля соответствующие методы Web API.

В статье рассмотрен пример интеграции с телефонией Гравител.

Подробнее о настройке интеграции при помощи пользовательского модуля читайте в справке ELMA365 TS SDK.

Предварительные настройки для интеграции с телефонией

Перед началом настройки:

  1. Проверьте, что ваш аккаунт у выбранного провайдера телефонии активен.
  2. Ознакомьтесь с условиями и сроком хранения записи звонка на сервере провайдера. В течение этого срока в интерфейсе ELMA365 пользователь может прослушать и загрузить запись себе на компьютер. Если запись удалена, появится сообщение об ошибке.
  3. Убедитесь, что в настройках провайдера можно указать Webhook системы. С его помощью будут обрабатываться HTTP-запросы о событиях телефонии. Если такой возможности нет, создайте промежуточный сервис, который преобразует данные провайдера в запрос на сервер ELMA365. Подробнее читайте в статье «Переносимые сервисы в модулях».
  4. Для совершения и приёма звонков на рабочих местах сотрудников убедитесь, что провайдер предоставляет SIP-софтфон. В противном случае установите облачную или локальную программу, которая поддерживает SIP-протокол и обеспечивает голосовую связь, например, MicroSip.
  5. Чтобы использовать телефонию для работы с продажами, активируйте одно из системных решений CRM. Без лицензии функциональные возможности CRM недоступны.

Создать пользовательский модуль интеграции с телефонией

  1. Перейдите в раздел Администрирование > Модули и в правом верхнем углу нажмите кнопку + Модуль.
  2. В открывшемся окне выберите опцию Создать, введите название и описание модуля. Нажмите кнопку Создать.
  3. На странице управления модулем перейдите на вкладку Методы API и нажмите кнопку Редактировать. На открывшейся странице перейдите на вкладку Скрипты.
  4. Вставьте код-заготовку для методов, которые реализуют интеграцию и обмен данными с телефонией.

// Проверить соединение с телефонией
async function VoipTestConnection(): Promise<VoipTestConnectionResult> {
   return {
       success: false,
       failReason: 'Функция проверки соединения не реализована.',
   };
}
// Обработать запрос от провайдера IP-телефонии
async function VoipParseWebhookRequest(request: FetchRequest): Promise<VoipWebhookParseResult> {
   return {
       response: new HttpResponse()
           .status(400)
           .content('Hello, world!'),
   };

// Получить список пользователей IP-телефонии
async function VoipGetMembers(): Promise<VoipMember[]> {
   throw new Error('Функция не реализована.');
}
// Сгенерировать звонок
async function VoipGenerateCall(srcPhone: string, dstPhone: string): Promise<void> {
   throw new Error('Функция не реализована.');
}
// Получить ссылку на запись звонка
async function VoipGetCallLink(callData: any): Promise<string> {
   throw new Error('Функция не реализована.');
}

  1. Сохраните и опубликуйте изменения.

На странице модуля отобразятся разделы:

  • Настройки телефонии, где указаны автоматически сгенерированные токен и Webhook;
  • Настройки обработки входящего звонка, где по умолчанию выбрано приложение для хранения контактных данных клиентов.

IP-Tel-integration-1

Соединить модуль с провайдером телефонии

Рассмотрим настройку интеграции пользовательского модуля с провайдером на примере телефонии Гравител.

  1. На странице модуля перейдите на вкладку Настройки и добавьте два строковых поля Ключ для авторизации в облачной АТС (apiKey) и Адрес облачной АТС (endpoint). В эти поля позднее вы запишете данные из настроек провайдера для подключения к Гравител.

IP-Tel-integration-3

  1. Вернитесь на страницу модуля — на ней появятся добавленные в предыдущем пункте поля. Cкопируйте значения полей Webhook URL и Токен.
  2. Перейдите в панель управления Гравител. Интеграция настраивается по Web API Гравител предыдущего поколения. В зависимости от версии личного кабинета, которую вы используете, выполните следующие шаги:
  • для актуальной версии — перейдите в Интеграции и выберите блок API Гравител. Нажмите Установить;
  • для предыдущей версии — перейдите в Интеграция с CRM и на открывшейся странице в правом нижнем углу выберите Интеграции с вашей CRM.
  1. На открывшейся странице заполните поля Название интеграции, Ваша CRM, Адрес вашей CRM и Ключ для авторизации в вашей CRM значениями из настроек модуля.

Затем скопируйте значения полей Ключ для авторизации в Облачной АТС и Адрес Облачной АТС. Перейдите в ELMA365 и вставьте их в соответствующие поля на странице модуля.

IP-Tel-integration-6

Подробнее о настройке интеграции в личном кабинете Гравител читайте в статье «Телефония Гравител».

  1. Сохраните настройки в панели управления Гравител и на странице модуля.
  2. Перейдите на страницу управления модулем и откройте вкладку Методы API > Скрипты. Нажмите кнопку Редактировать и добавьте в начало скрипта следующий код:

// Загрузить значение поля Ключ для авторизации в облачной АТС из настроек модуля
function getApiKey() {
   const apiKey = Namespace.params.data.apiKey;
   if (!apiKey) {
       throw new Error('invalid API key');
   }
   return apiKey;  
}
// Загрузить значение поля Адрес облачной АТС из настроек модуля
function getEndpoint() {
   const endpoint = Namespace.params.data.endpoint;
   if (!endpoint) {
       throw new Error('invalid endpoint URL');
   }
   return endpoint;
}
// Выполнить запрос к Гравител
async function fetchEndpoint(command: string, body: Object = {}): Promise<FetchResponse> {
   return fetch(getEndpoint(), {
       method: 'POST',   
       headers: {
           'Content-Type': 'application/json',
       },
       body: JSON.stringify({
           cmd: command,
           token: getApiKey(),
           ...body,
       }),
   });
}
// Распарсить контент с типом application/x-www-form-urlencoded
function parseUrlEncoded(body: string): Record<string, string> {
    const result: Record<string, string> = {};
    for (const pair of body.split('&')) {
        const keyValue = pair.split('=');
        if (keyValue.length === 2) {
            const key = decodeURIComponent(keyValue[0]);
            const value = decodeURIComponent(keyValue[1]);
            result[key] = value;
        }
    }
    return result;
}
// Сообщение от Гравител
interface GravitelWebhookRequest {
    cmd: string; // Тип операции
    type: string; // Тип события, связанного со звонком
    phone: string; // Номер телефона клиента (в формате E.164)
    ext?: string; // Внутренний номер пользователя облачной АТС, если есть
}
// Данные, передающиеся с командой history
interface GravitelWebhookHistoryCommand extends GravitelWebhookRequest {
    duration: string; // Общая длительность звонка в секундах
    callid: string; // Уникальный id звонка
    link?: string; // Ссылка на запись звонка, если она включена в Облачной АТС
    status: string; // Статус входящего или исходящего звонка
}
// Данные, которые сохраняются с каждым звонком
interface GravitelCallData {
    id: string;
    link: string;
}
// Сконвертировать статус звонка Гравител в статус звонка ELMA365
function toCallDisposition(status: string): VoipCallDisposition {
    switch (status) {
        case 'Success':
            return VoipCallDisposition.Answered;
        case 'Busy':
            return VoipCallDisposition.Busy;
        case 'Missed':
            return VoipCallDisposition.Cancel;
        case 'NotAvailable':
            return VoipCallDisposition.NoAnswer;
        case 'NotAllowed':
        default:
            return VoipCallDisposition.Unknown;
    }
}

  1. Добавьте или замените метод VoipTestConnection, который позволяет проверить соединение с телефонией. В данном примере тестируется команда для получения списка пользователей. Вы можете указать другую команду.

// Проверить соединение к телефонии
async function VoipTestConnection(): Promise<VoipTestConnectionResult> {
   try {
       // Выполняем команду для получения списка пользователей телефонии
       const response = await fetchEndpoint('accounts');
       const message = await response.json();
       if (response.status !== 200) {
           throw new Error(`${response.status} ${response.statusText}: ${message.error}`);
       }
       return { success: true };
   } catch (e) {
       return {
           success: false,
           failReason: e.message,
       };
   }
}

  1. Сохраните и опубликуйте скрипты, после чего перейдите на страницу модуля и нажмите кнопку Проверить соединение. Появится сообщение об успешном подключении к телефонии.

Если вы видите ошибку при проверке соединения:

  • убедитесь, что поля на странице модуля и в панели управления Гравител заполнены корректно. Например, ошибка «Invalid token» означает, что неверно заполнено поле Токен;
  • проверьте, что скрипты составлены без ошибок, а также успешно сохранены и опубликованы;
  • убедитесь, что ваш аккаунт Гравител активен.
  1. После этого добавьте или замените следующие методы для интеграции с Гравител:
  • метод VoipParseWebhookRequest — позволяет модулю обработать данные из HTTP-запроса от провайдера и передать их в ELMA365;

// Обработать запрос от провайдера IP-телефонии
async function VoipParseWebhookRequest(request: FetchRequest): Promise<VoipWebhookParseResult> {
    try {
        const headers = request.headers;
        const contentType = headers ? headers['Content-Type'] : undefined;
        if (contentType !== 'application/x-www-form-urlencoded') {
            throw new Error(`Expected Content-Type to be "application/x-www-form-urlencoded" (got: "${contentType}"`);
        }
        if (typeof request.body !== 'string') {
            throw new Error('Expected request body to be string')
        }
        let event: VoipWebhookRequest | undefined;
        let callRecord: VoipCallRecord | undefined;
        let response: HttpResponse | undefined;
        const data = <GravitelWebhookRequest><unknown> parseUrlEncoded(request.body);
        switch (data.cmd) {
            case 'event': {
// Облачная АТС отправляет в вашу CRM уведомления о событиях входящих звонков пользователям: 
появлении, принятии или завершении звонка. Команда может быть использована для отображения 
всплывающей карточки клиента в интерфейсе CRM
                const dstPhone = data.ext ?? '';
                switch (data.type) {
                    case 'INCOMING': {
// Пришёл входящий звонок. В это время у менеджера должен начать звонить телефон
                        event = {
                            event: VoipWebhookEvent.NotifyStart,
                            direction: VoipCallDirection.In,
                            dstPhone: dstPhone,
                            srcPhone: data.phone,
                            disposition: VoipCallDisposition.Unknown,
                        };
                    } break;
                    case 'ACCEPTED': {
// Звонок успешно принят, т. е. менеджер снял трубку. В этот момент можно убрать всплывающую 
карточку контакта в CRM
                        event = {
                            event: VoipWebhookEvent.NotifyAnswer,
                            direction: VoipCallDirection.In,
                            dstPhone: dstPhone,
                            srcPhone: data.phone,
                            disposition: VoipCallDisposition.Unknown,
                        }
                    } break;
                    case 'COMPLETED': {
// Звонок успешно завершён, т. е. менеджер или клиент положили трубку после разговора
                        event = {
                            event: VoipWebhookEvent.NotifyEnd,
                            direction: VoipCallDirection.In,
                            dstPhone: dstPhone,
                            srcPhone: data.phone,
                            disposition: VoipCallDisposition.Unknown,
                        }
                    } break;
                    case 'CANCELLED': {
// Звонок сброшен, т.е. клиент не дождался пока менеджер снимет трубку. Либо, если это был 
звонок сразу на группу менеджеров, на звонок мог ответить кто-то ещё
                        event = {
                            event: VoipWebhookEvent.NotifyEnd,
                            direction: VoipCallDirection.In,
                            dstPhone: dstPhone,
                            srcPhone: data.phone,
                            disposition: VoipCallDisposition.Cancel, 
                        }
                    } break;
                    case 'OUTGOING': {
// Менеджер совершает исходящий звонок. В это время облачная АТС пытается дозвониться до клиента
                        event = {
                            event: VoipWebhookEvent.NotifyStart,
                            direction: VoipCallDirection.Out,
                            dstPhone: dstPhone,
                            srcPhone: data.phone,
                            disposition: VoipCallDisposition.Unknown, 
                        }
                    } break;
                    default: throw new Error(`Unknown event type "${data.type}"`)
                }
            } break;
            case 'history': {
// После успешного звонка в CRM отправляется запрос с данными о звонке и ссылкой на запись 
разговора. Команда может быть использована для сохранения в данных ваших клиентов истории 
и записей входящих и исходящих звонков
                const cmd = <GravitelWebhookHistoryCommand> data;
                callRecord = {
                   srcPhone: cmd.phone,
                   dstPhone: cmd.ext ?? '',
                   direction: cmd.type === 'out' ? VoipCallDirection.Out : VoipCallDirection.In,
                   duration: parseInt(cmd.duration),
// Данные из этого поля будут доступны в функции VoipGetCallLink
                    call: <GravitelCallData>{
                        link: cmd.link,
                        id: cmd.callid,
                    },
                    disposition: toCallDisposition(cmd.status),
                }
            } break;
        }
        return { 
            event: event,
            callRecord: callRecord,
            response: response,
        };
    } catch (e) {
        return {
            response: new HttpResponse()
                .status(400)
                .content(JSON.stringify({
                    error: e.message ?? 'internal error',
                }))
        };
    }
}

  • метод VoipGetMembers — позволяет получить данные о пользователях телефонии. Эти данные отображаются на странице модуля при сопоставлении пользователей провайдера и пользователей ELMA365;

// Получить список пользователей от провайдера IP-телефонии
async function VoipGetMembers(): Promise<VoipMember[]> {
    const response = await fetchEndpoint('accounts');
    if (response.status !== 200) {
        throw new Error(`received error response ${response.status}: ${response.statusText}`);
    }
    interface GravitelVoipUser {
        name: string;
        ext: string;
    }
    const voipUsers = <GravitelVoipUser[]> (await response.json());
    return voipUsers.map(user => ({
        id: user.ext,
        label: user.name,
    }));
}

  • метод VoipGenerateCall — позволяет инициировать звонок из интерфейса ELMA365;

// Сгенерировать звонок
async function VoipGenerateCall(srcPhone: string, dstPhone: string): Promise<void> {
    const response = await fetchEndpoint('makeCall', {
        phone: dstPhone,
        user: srcPhone,
    });
    if (response.status !== 200) {
        throw new Error(`received error response ${response.status}: ${response.statusText}`);
    }
}

  • метод VoipGetCallLink — позволяет сохранить запись разговора. В качестве аргумента передаются данные из поля call параметра VoipCallRecord (заполняется в методе VoipParseWebhookRequest).

// Получить ссылку на запись разговора
async function VoipGetCallLink(callData: GravitelCallData): Promise<string> {
    return callData.link;
}

  1. Чтобы реализовать функциональность кнопок для обработки звонка в его карточке, добавьте методы, перечисленные в справке ELMA365 TS SDK.
  2. Сохраните и опубликуйте скрипты.
  3. На странице модуля нажмите на появившуюся кнопку Настроить и сопоставьте номера пользователей из настроек провайдера с пользователями ELMA365. Подробнее читайте в статье «Телефония Гравител».

IP-Tel-integration-7

  1. Сохраните настройки.

Теперь интеграция с Гравител полностью функционирует.