Интеграция с IP-телефонией

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

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

  • инициация исходящего звонка из системы через контекстное меню поля типа Телефон;
  • отображение карточки звонка с краткой информацией по клиенту и кнопками управления вызовом, когда пользователь системы принимает входящий или совершает исходящий звонок;
  • открытие карточки сводной информации по клиенту;
  • автоматическое создание и сохранение элемента приложения, если звонок был пропущен, а номера нет в базе;
  • регистрация пропущенных звонков;
  • сохранение в ленту объекта сообщения о входящем или исходящем звонке. Если разговор состоялся, то к сообщению добавляется ссылка на запись звонка. Её можно воспроизвести в интерфейсе системы, а также оставить комментарий, содержащий, к примеру, резюме звонка;
  • автоматическое создание элемента приложения Звонок в разделе Телефония для каждого входящего и исходящего звонка;
  • сопоставление учётных записей пользователей IP-телефонии и пользователей системы для маршрутизации и корректного отражения информации об операторе, принявшем или осуществившем звонок.

Ограничения

Прежде чем реализовывать модуль интеграции, обратите внимание на следующие ограничения API:

  1. На стороне системы не осуществляется маршрутизация звонка во избежание конфликтов с настройками маршрутизации на стороне телефонии и на стороне системы. Такую возможность предоставляет практически каждый провайдер: после того, как звонок маршрутизирован на сотрудника или группу, входящий вызов отображается в системе для указанных пользователей.
  2. На текущий момент обработка событий телефонии производится путем доставки сообщений на определенную HTTP-ссылку (Webhook). Если провайдер телефонии не предоставляет возможность указать ссылку для доставки сообщений, то необходимо создать промежуточный сервис, который преобразует данные провайдера телефонии в запрос на сервер системы. Для этого вы можете создать переносимый сервис в модуле.
  3. Для записей звонков сохраняется только ссылка на файл. Обычно сами записи хранятся у провайдера телефонии. Он предоставляет только ссылку. Вам следует обратить внимание на политику хранения файлов на серверах провайдера телефонии. Файлы могут быть доступны ограниченное время.

Реализация

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

// Проверить соединение с телефонией. Вызывается при нажатии кнопки Проверить соединение на странице модуля
// Возвращает статус проверки соединения, который отобразится для пользователя на странице модуля
async function VoipTestConnection(): Promise<VoipTestConnectionResult> { }

// Обработать запрос от провайдера IP-телефонии. Вызывается, когда происходит запрос на вебхук модуля
// В параметре `request` содержится информация об HTTP-запросе, в том числе HTTP-заголовки и содержимое тела запроса
// Функция возвращает результат обработки запроса, на основании которого система будет показывать уведомления
// о входящем звонке, сохранять запись звонка и т. д.
async function VoipParseWebhookRequest(request: FetchRequest): Promise<VoipWebhookParseResult> { }

// Получить список пользователей со стороны провайдера IP-телефонии
// Вызывается при сопоставлении пользователей телефонии с пользователями системы после нажатия кнопки Настроить на странице модуля
// Возвращает список пользователей провайдера IP-телефонии
async function VoipGetMembers(): Promise<VoipMember[]> { }

// Сгенерировать исходящий звонок. Функция вызывается, когда пользователь системы нажимает кнопку звонка
// в контекстном меню поля типа Телефон или кнопку Перезвонить в карточке звонка. В параметре
// `srcPhone` указывается номер звонящего пользователя, в `dstPhone` — номер, на который нужно позвонить,
// `ext` — добавочный номер. Рекомендуется настроить АТС или SIP-софтфон таким образом, чтобы добавочный номер
// автоматически набирался тональным набором после ответа на вызов другой стороной.
async function VoipGenerateCall(srcPhone: string, dstPhone: string, ext?: string): Promise<void> { }

// Получить ссылку на запись звонка. Функция вызывается, когда пользователь системы проигрывает запись звонка из
// ленты элемента приложения или при сохранении ссылки на запись звонка в контекст элемента приложения Звонок
// раздела Телефония. В параметре `callData` содержатся данные, которые были указаны при сохранении
// информации о записи звонка из функции `VoipParseWebhookRequest`. Функция должна возвращать ссылку на файл
async function VoipGetCallLink(callData: any): Promise<string> { }

// Вызывается автоматически при изменении ссылки на вебхук (например, при обновлении токена)
// В параметре `webhookUrl` указывается абсолютная ссылка на вебхук модуля интеграции. Функция не обязательна к реализации
async function VoipOnWebhookUpdated(webhookUrl: string): Promise<void> { }

После реализации этих функций и сохранения скрипта на странице модуля вы найдете новый раздел под названием Настройки телефонии. В этом разделе будет указана ссылка на вебхук — при отправке данных на эту ссылку будет вызываться функция VoipParseWebhookRequest из скрипта. В этой функции вы должны обработать запрос и вернуть из неё экземпляр VoipWebhookParseResult, на основании которого система отобразит уведомления о входящем звонке или сохранит информацию о записи звонка. Саму ссылку на вебхук вам необходимо указать в настройках вашего провайдера телефонии. Ссылка обрабатывает HTTP-запросы типа GET и POST. Также в ссылке должен присутствовать обязательный параметр token.

Рекомендуем вам ознакомиться со статьей справки «Интеграция с IP-телефонией через пользовательский модуль». Внутри статьи вы найдете заготовку модуля и пример реального использования API на основе интеграции с IP-телефонией Гравител.

Реализация поддержки кнопок в карточке звонка

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

// Принять звонок. Функция вызывается, когда в карточке звонка пользователь системы нажимает кнопку Принять звонок
// После этого начинают передаваться голосовые данные между оператором и клиентом
// В параметре `srcPhone` указывается номер телефона звонящего, в `dstPhone` — номер, на который производится звонок
async function VoipAcceptCall(srcPhone: string, dstPhone: string): Promise<void> { }

// Завершить звонок. Функция вызывается, когда в карточке звонка пользователь системы нажимает кнопку Завершить звонок
// Звонок между оператором и клиентом завершается
// В параметре `srcPhone` указывается номер телефона звонящего, в `dstPhone` — номер, на который производится звонок
async function VoipHangupCall(srcPhone: string, dstPhone: string): Promise<void> { }

// Переназначить звонок. Функция вызывается, когда в карточке звонка пользователь системы нажимает кнопку Переназначить
// Звонок текущего оператора завершается, и клиент соединяется с другим оператором
// В параметре `srcPhone` указывается номер телефона звонящего, в `dstPhone` — номер, на который производится звонок,
// `redirectTo` — внутренний номер оператора, на которого переназначается звонок
async function VoipRedirectCall(srcPhone: string, dstPhone: string, redirectTo: string): Promise<void> { }

// Поставить звонок на ожидание. Функция вызывается, когда в карточке звонка пользователь системы
// нажимает кнопку Поставить на ожидание. Передача голосовых данных между оператором и клиентом прекращается без завершения звонка
// В параметре `srcPhone` указывается номер телефона звонящего, в `dstPhone` — номер, на который производится звонок,
// `hold` — значение `true`. Оно указывает, что нужно поставить звонок на ожидание, иначе — снять с ожидания
async function VoipHoldCall(srcPhone: string, dstPhone: string, hold: boolean): Promise<void> { }

Если в пользовательском модуле телефонии функция не реализована, соответствующая кнопка не будет отображаться в карточке звонка.

Реализация поддержки CTI-панели для звонков через браузер

Если провайдер телефонии поддерживает технологию WebRTC, то можно совершать звонки напрямую из браузера с помощью встроенной в систему CTI-панели. Она позволяет работать со звонками из единого интерфейса. Установка SIP-софтфона в таком случае не нужна.

Функционал CTI-панели доступен только при наличии у пользователя лицензии ELMA365 Omni отдельно или в составе другой лицензии.

Взаимодействие CTI-панели и телефонии

Интеграция телефонии в CTI-панели системы реализуется через виджет пользовательского модуля. Когда сотрудник выбирает провайдера телефонии в CTI-панели, автоматически создаётся экземпляр виджета. В его клиентских скриптах вызывается функция connect для инициализации подключения к провайдеру телефонии. Виджет позволяет:

  • устанавливать канал связи для обмена сообщениями протокола сигнализации. Например, для телефонии Asterisk используется протокол SIP over WebSocket (RFC 7118), позволяющий передавать SIP-сигнализацию через WebSocket-соединение;
  • обрабатывать события от CTI-панели и отправлять их в телефонию. Например, когда пользователь системы нажимает кнопку исходящего звонка в CTI-панели, вызывается метод BrowserVoipClient.call. В результате, в виджете должен инициироваться исходящий звонок по протоколу телефонии, а в систему возвращаться объект с информацией о звонке. Подробнее о типах данных читайте в примере ниже;
  • обрабатывать события от телефонии и отправлять их в CTI-панель. Например, когда от провайдера телефонии поступает сигнал о новом входящем звонке, в виджете должен вызываться метод BrowserVoipEventSink.callReceived, чтобы в CTI-панели отобразилась информация о входящем звонке;
  • настраивать WebRTC-соединение для передачи медиапотока между браузером пользователя и телефонией. В частности, в виджете должен выполняться обмен SDP-описаниями и ICE-кандидатами, а также должно контролироваться состояние WebRTC-сессии.

Настройка пользовательского виджета для работы с CTI-панелью

Для реализации виджета взаимодействия с CTI-панелью выполните следующие шаги:

  1. В пользовательском модуле телефонии создайте виджет со следующими параметрами:

    • Название — добавьте имя виджета;
    • Код виджета — укажите значение browser_voip_client. По этому коду в системе определяется, какой конкретно виджет из модуля нужно использовать для интеграции с CTI-панелью;
    • Расширение — оставьте пустым.
  2. В открывшемся дизайнере интерфейсов на вкладке Скрипты добавьте клиентские скрипты с описанием интерфейсов для работы с CTI-панелью:

    • данные для подключения к телефонии:

      interface BrowserVoipClientCredentials {
           // Внутренний номер пользователя телефонии
           extension: string;
      
           // Логин пользователя телефонии
           username: string;
      
           // Пароль пользователя телефонии
           password: string;
      }
      
    • управление звонками через CTI-панель. Все методы интерфейса BrowserVoipClient, кроме getCallId, обязательны для реализации:

      // Установленное подключение к телефонии
      // @template TIncomingCall Входящий звонок
      // @template TOutgoingCall Исходящий звонок
      interface BrowserVoipClient<TIncomingCall = unknown, TOutgoingCall = unknown> {
           // Отключиться от телефонии. Вызывается системой, когда пользователь выключает CTI-панель
           disconnect(): Promise<void>;
      
           // Совершить исходящий звонок. Вызывается системой, когда пользователь вводит номер
           // в номеронабирателе и нажимает кнопку Совершить исходящий звонок. Также может
           // вызываться при звонке через контекстное меню поля Номер телефона
           // @param phoneNumber Номер телефона, на который нужно позвонить
           // @param ext Добавочный номер, который нужно автоматически набрать в тональном режиме
           // после установки соединения
           // @returns Объект, представляющий собой звонок. В этом объекте виджет может хранить
           // любые данные звонка
           // Объект будет передан в другие методы интерфейса `BrowserVoipClient`, когда в CTI-панели нужно
           // изменить состояние звонка
           call(phoneNumber: string, ext?: string): Promise<TOutgoingCall>;
      
           // Принять входящий звонок. Вызывается системой, когда пользователь нажал кнопку
           // принятия входящего звонка. Она появляется после того, как пришло событие телефонии о
           // новом входящем звонке. Подробнее читайте в описании `BrowserVoipEventSink.callReceived`
           // @param invite Входящий звонок, который был указан в аргументе
           // метода `BrowserVoipEventSink.callReceived`
           accept(invite: TIncomingCall): void;
      
           // Завершить звонок, т. е. повесить трубку. Также вызывается, если нужно отклонить входящий звонок
           // @param call Звонок
           hangup(call: TIncomingCall | TOutgoingCall): void;
      
           // Поставить звонок на удержание
           // @param call Звонок
           hold(call: TIncomingCall | TOutgoingCall): void;
      
           // Снять звонок с удержания
           // @param call Звонок
           unhold(call: TIncomingCall | TOutgoingCall): void;
      
           // Произвести слепой перевод (blind transfer) на указанный номер телефона. В результате вызова метода
           // ожидается, что переданный звонок будет завершён
           // @param call Звонок
           // @param redirectPhoneNumber Номер телефона, на который нужно произвести слепой перевод
           blindTransfer(call: TIncomingCall | TOutgoingCall, redirectPhoneNumber: string): void;
      
           // Завершить перевод с сопровождением (attended transfer). В результате вызова метода ожидается, что
           // два указанных абонента будут соединены друг с другом, а звонок `transferor` перейдёт в
           // состояние Звонок завершён
           // @param transferor Звонок текущего пользователя с клиентом, которого нужно перевести
           // на сотрудника `caller`
           // @param caller Звонок текущего пользователя с сотрудником, на которого будет переведён звонок
           attendedTransfer(transferor: TIncomingCall | TOutgoingCall, caller: TIncomingCall | TOutgoingCall): void;
      
           // Отправить DTMF-сигнал. Вызывается системой при вводе пользователем DTMF-кода в интерфейсе CTI-панели
           // @param call Звонок
           // @param key Сигнал для отправки. Например: '1', '5', '*', '#'
           // @returns Promise, который разрешается после отправки сигнала
           sendDtmf(call: TIncomingCall | TOutgoingCall, key: string): Promise<void>;
      
           // Получить уникальный идентификатор звонка на стороне телефонии. Данный идентификатор должен быть
           // одинаковым на стороне клиента в браузере и в событии, которое генерируется функцией
           // `VoipParseWebhookRequest` при обработке события через вебхук. ID звонка используется системой для
           // обновления информации о звонке дополнительными данными с сервера. Например, к звонку в CTI-панели
           // может быть привязан элемент приложения Звонок раздела Телефония
           // @param call Звонок
           // @returns Идентификатор звонка или null, если идентификатор недоступен
           getCallId?(call: TIncomingCall | TOutgoingCall): string | null;
      }
      
    • отправка уведомлений в CTI-панель:

      // Объект, через который соединение с телефонией `BrowserVoipClient` может отправлять уведомления
      // об изменении состояния звонков в CTI-панель
      // @template TIncomingCall Входящий звонок
      // @template TOutgoingCall Исходящий звонок
      interface BrowserVoipEventSink<TIncomingCall = unknown, TOutgoingCall = unknown> {
           // Уведомить о новом входящем звонке. В CTI-панели пользователю отобразится предложение
           // принять или отклонить входящий звонок
           // @param invite Объект с информацией о входящем звонке. Это значение будет передано как есть в
           // `BrowserVoipClient.accept` или `BrowserVoipClient.hangup`
           // @param phoneNumber Номер телефона, с которого нам звонят
           callReceived(invite: TIncomingCall, phoneNumber: string): void;
      
           // Уведомить о завершении звонка. В CTI-панели звонок перейдёт в состояние Звонок завершён
           // @param call Звонок
           callFinished(call: TIncomingCall | TOutgoingCall): void;
      
           // Уведомить об успешной установке соединения между двумя абонентами для указанного звонка.
           // Ожидается, что оба абонента могут слышать друг друга. В CTI-панели звонок перейдёт в состояние
           // Идёт разговор
           // @param call Звонок
           callEstablished(call: TIncomingCall | TOutgoingCall): void;
      
           // Отправить событие потери соединения с телефонией. Этот метод следует вызывать, когда
           // произошла критическая ошибка, из-за которой автоматически восстановить соединение
           // с телефонией невозможно
           // @param reason Причина ошибки, которую увидит пользователь
           disconnected(reason: string): void;
      }
      
  3. Также на вкладке Скрипты в клиентских скриптах реализуйте функцию connect. Она вызывается автоматически после того, как пользователь системы включит CTI-панель. При успешном подключении к телефонии с помощью этой функции возвращается реализация интерфейса BrowserVoipClient. В дальнейшем в системе будут вызываться методы этого объекта в зависимости от действий пользователя в CTI-панели. Метод также принимает параметр типа BrowserVoipClientCredentials, в котором находится внутренний номер, логин и пароль для подключения к телефонии. Администратор системы заполняет эту информацию на странице настроек модуля телефонии. Объект BrowserVoipEventSink используется для отправки событий в CTI-панель, например, получение нового входящего звонка или изменение состояния звонка.

    async function connect(credentials: BrowserVoipClientCredentials, eventSink: BrowserVoipEventSink): Promise<BrowserVoipClient> {
        const client: BrowserVoipClient = {
            // Реализуйте интерфейс BrowserVoipClient
        };
        try {
            // Подключитесь к каналу сигнализации телефонии, используя данные из аргумента `credentials`
            ...
            // На момент возврата данных из метода экземпляр `BrowserVoipClient` должен быть подключён к телефонии
            return client;
        } catch {
            // В случае ошибки соединения закройте все подключения к телефонии
            ...
            // Пробросьте исключение, чтобы в CTI-панели пользователь увидел
            // сообщение об ошибке подключения телефонии
            throw;
        }
    }
    
  4. Сохраните и опубликуйте виджет. После этого при включённом модуле пользователи смогут выбрать провайдера телефонии в CTI-панели.

Автоматическая аутентификация в телефонии (SSO)

Чтобы сотрудники могли подключаться к телефонии через CTI-панель, администратор системы должен заполнить в настройках пользовательского модуля логин и пароль для каждого пользователя телефонии. При большом количестве сотрудников это может быть трудоёмко. Чтобы повысить безопасность и снизить нагрузку на администратора системы, рекомендуется автоматически получать учётные данные из телефонии или стороннего сервиса. Схема работы может выглядеть следующим образом:

  1. На вкладке Настройки пользовательского модуля телефонии создайте поля со следующими кодами:
    • voip_supports_single_sign_on с типом данных Выбор «да/нет». Укажите значение по умолчанию для этого поля — Да или вынесите его на форму настроек модуля. Когда в этом поле указано значение Да, в системе не отображаются поля для ввода логина и пароля пользователя телефонии при настройке CTI-панели;
    • api_key с типом данных Строка. В этом поле хранится API-ключ для аутентификации в телефонии.
  2. Откройте виджет интеграции с CTI-панелью в дизайнере интерфейсов и на вкладке Контекст добавьте переменные со следующими кодами:
    • extension с типом данных Строка. Используется, чтобы передать внутренний номер пользователя, для которого нужно получить токен;
    • auth_token с типом данных Строка. Используется, чтобы получить токен для подключения к телефонии.
  3. На вкладке Скрипты в метод connect клиентских скриптов добавьте вызов серверной функции. С её помощью отправляется запрос на получение учётных данных для подключения к телефонии. Текущего пользователя можно получить вызовом метода System.users.getCurrentUser(). Полученные учётные данные сохраните в контекстной переменной виджета auth_token.
  4. В клиентских скриптах получите результат и используйте эти данные для подключения к телефонии.
// Клиентские скрипты
async function connect(credentials: BrowserVoipClientCredentials, eventSink: BrowserVoipEventSink): Promise<BrowserVoipClient> {
    // Сохраняем внутренний номер пользователя в переменную, чтобы серверный скрипт имел к нему доступ
    Context.data.extension = credentials.extension;
    await Server.rpc.getAuthToken();
    // Вызов getAuthToken() сохранит токен в контекстной переменной
    const token = Context.data.auth_token;
    if (!token) {
        throw new Error('Не удалось получить токен авторизации');
    }
    // Используйте `token` для подключения к телефонии
    ...
}
// Серверные скрипты
async function getAuthToken(): Promise<void> {
    const currentUser = await System.users.getCurrentUser();
    const extension = Context.data.extension;
    // API-ключ для стороннего сервиса, обычно заполняется на странице настроек модуля
    const apiKey = (await Namespace.getParams()).data.api_key;
    const response = await fetch('https://example.com/auth', {
        method: 'POST',
        body: JSON.stringify({
            userId: currentUser.id,
            extension: extension,
            apiKey: apiKey,
        }),
    });
    if (response.ok) {
        const token = await response.text();
        Context.data.auth_token = token;
    }
}