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

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

Для того чтобы пользовательский модуль реализовывал подключение к телефонии, необходимо в Сценариях модуля внести следующие функции:

// Проверить соединение к телефонии (вызывается по нажатию на кнопку "Проверить соединение" на странице модуля).
async function VoipTestConnection(): Promise<VoipTestConnectionResult>
 
// Обработать запрос от провайдера IP-телефонии.
async function VoipParseWebhookRequest(request: FetchRequest): Promise<VoipWebhookParseResult>
 
// Получить список пользователей IP-телефонии (используется для сопоставления пользователей по нажатию кнопки "Настроить" на странице модуля).
async function VoipGetMembers(): Promise<VoipMember[]>
 
// Сгенерировать звонок.
async function VoipGenerateCall(srcPhone: string, dstPhone: string): Promise<void>
 
// Получить ссылку на запись звонка.
async function VoipGetCallLink(callData: any): Promise<string>
 
// Вызывать автоматически при изменении ссылки на webhook (например, при обновлении токена).
// Эту функцию не обязательно реализовывать.
async function VoipOnWebhookUpdated(webhookUrl: string): Promise<void>

Сохраните и опубликуйте скрипты, после чего на странице модуля появится форма Настройки телефонии.

IP-Tel-integration-1

Чтобы провайдер IP-телефонии мог отправлять запросы в ваш модуль ELMA365, необходимо в панели администрирования провайдера IP-телефонии заполнить адрес обратной связи (Webhook). Этот адрес указан в поле Webhook URL на странице модуля. Чтобы скопировать адрес, нажмите на значок рядом с текстовым полем. Некоторые провайдеры телефонии также требуют заполнить отдельное поле для ключа авторизации в CRM – значение этого ключа указано в поле Токен на странице модуля.

Перед созданием пользовательского модуля интеграции убедитесь, что в каталоге ELMA365 Store нет нужного вам модуля. На данный момент в каталоге доступны следующие модули:

Обратите внимание, система телефонии Asterisk доступна только для ELMA365 On-Premises.

Далее приведены заготовка интеграции и пример на основе интеграции провайдера IP-телефонии Гравител.

Заготовка интеграции

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

  1. Перейдите в Администрирование > Модули.
  2. Создайте новый пользовательский модуль. Для этого:
    1. Нажмите кнопку +Модуль.
    2. В открывшемся окне выберите опцию Создать.
    3. Введите название и описание модуля.
    4. Нажмите Создать.
  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. Нажмите кнопки Сохранить и Опубликовать в верхнем левом углу страницы.

 

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

IP-Tel-integration-1

Пример интеграции с Гравител

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

IP-Tel-integration-3

  1. Вернитесь на страницу модуля — на ней появятся добавленные в предыдущем пункте поля:

IP-Tel-integration-4

  1. Перейдите в панель управления Гравител, зайдите в раздел Настройки и выберите вкладку Интеграция с CRM. Нажмите на блок Добавить в конце списка.
  2. На открывшейся странице заполните поля Имя вашей CRM, Адрес вашей CRM и Ключ для авторизации в вашей CRM. Для этого скопируйте значения полей Webhook URL и Токен со страницы модуля.

IP-Tel-integration-5

  1. Скопируйте со страницы настройки интеграции в Гравител значения полей Ключ для авторизации в Облачной АТС и Адрес Облачной АТС и вставьте в соответствующие поля на странице настройки модуля. Нажмите Сохранить. Также не забудьте сохранить интеграцию в Гравител.

IP-Tel-integration-6

  1. Перейдите в редактор сценариев (Настройки модуля > Методы 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;
    // Номер телефона клиента (в формате E.164)
    phone: string;
    // Внутренний номер пользователя облачной АТС, если есть
    ext?: string;
}
// Данные, передающиеся с командой "history". 
interface GravitelWebhookHistoryCommand extends GravitelWebhookRequest {
    // Общая длительность звонка в секундах
    duration: string;
    // Уникальный id звонка
    callid: string;
    // Ссылка на запись звонка, если она включена в Облачной АТС
    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. Сохраните и опубликуйте скрипты, после чего перейдите на страницу модуля и нажмите кнопку Проверить соединение. Появится сообщение об успешном подключении к телефонии.

IP-Tel-integration-7

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

  • корректность заполнения настроек подключения к Гравител на странице модуля и в панели управления Гравител. Например, ошибка 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',
                }))
        };
    }
}

  1. Получение пользователей провайдера телефонии происходит в функции VoipGetMembers. Данные из функции используются в виджете сопоставления пользователей провайдера телефонии и пользователей ELMA365, доступном по нажатию на кнопку Настроить на странице пользовательского модуля.

// Получить список пользователей от провайдера IP телефонии.
// Это внутренние пользователи - внутренние номера/SIPID, нужно для сопоставления между пользователями IP-провайдера и пользователями ELMA365
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,
    }));
}

  1. Функция 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}`);
    }
}

  1. Функция VoipGetCallLink используется для получения ссылки на запись разговора. В качестве аргумента функции передаются данные поля call интерфейса VoipCallRecord (заполняется в VoipParseWebhookRequest).

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

  1. Сохраните и опубликуйте скрипты. Теперь интеграция с Гравител полностью функционирует.