...

Интеграция с IP-телефонией РТУ (часть 1)

Тема в разделе "Примеры решений и дополнительных модулей", создана пользователем vyimova, 6 авг 2024.

  1. vyimova

    vyimova Участник

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

    Прежде всего создадим новый модуль. Затем нужно сделать «заготовку» в Методах API модуля:
    Код:
    
    // Проверить соединение к телефонии (вызывается по нажатию на кнопку "Проверить соединение" на странице модуля).
    async function VoipTestConnection(): Promise<VoipTestConnectionResult>

    // Обработать запрос от провайдера IP-телефонии.
    async function VoipParseWebhookRequest(requestFetchRequest): Promise<VoipWebhookParseResult>

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

    // Сгенерировать звонок.
    async function VoipGenerateCall(srcPhonestringdstPhonestring): Promise<void>

    // Получить ссылку на запись звонка.
    async function VoipGetCallLink(callDataany): Promise<string>

    // Вызывать автоматически при изменении ссылки на webhook (например, при обновлении токена).
    // Эту функцию не обязательно реализовывать.
    async function VoipOnWebhookUpdated(webhookUrlstring): Promise<void>
    После публикации данного скрипта, на странице модуля должны появиться Настройки телефонии.

    [​IMG]

    Из данной формы можно:

    • получить информацию о Webhook URL;
    • получить информацию о Токене (также он входит в состав Webhook URL как параметр «token»);
    • проверить доступ к телефонии;
    • настроить сопоставление пользователей ELMA365 с пользователями телефонии;
    • настроить обработку входящих звонков: связать с конкретным приложением, указать поле с контактным телефоном (из связанного на предыдущем пункте приложения) и выбрать поля для отображения на карточке звонка.
    Кроме того, можно как в любом другом модуле создать поля в контексте и вынести их на форму модуля.

    [​IMG]

    Особенности системы

    Для разработки интеграции с РТУ было необходимо проработать возможность использования нескольких доменов, которые в том числе участвовали в процессе авторизации. Домен и пароль для данного домена кодируются в заголовке Authorization запроса в Base64 (Basic-авторизация).

    Поэтому для хранения данных о домене и соответствующем пароле создадим таблицу доменов в настройках модуля. Также в таблице доменов создадим поля Добавочный номер ELMA для корректной работы сопоставления пользователей и Примечание для нужд пользователей. При этом добавочный номер может содержать только числа – т.к. нам этот номер нужен для идентификации пользователей. В разных доменах могут пересекаться коды пользователей. Поэтому для их точной идентификации пользователя нужен указатель на домен. Платформа нормализует id пользователя, оставляя только цифры.

    Помимо таблицы создадим строковое поле для хранения URL телефонии РТУ.

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

    Первым для настройки методом является проверка соединения с телефонией, который может проверить корректный ввод данных и наличие доступа к телефонии.

    В данном примере по ТЗ было задание получить в ответ на попытку соединения текст с ошибкой 401 Unauthorized.

    Отредактируем Методы API следующим образом:
    Код:
    
    // проверить соединение к телефонии (вызывается по нажатию на кнопку "Проверить соединение" на странице модуля)
    async function VoipTestConnection(): Promise<VoipTestConnectionResult> {
    try {
       
    let response await fetch(`${Namespace.params.data.api_endpoint}`, {
             
    method'GET'
           
    });
       const 
    message await response.json();
       if (
    response.status !== 200) {
           throw new 
    Error(`${response.status} ${response.statusText}${message.error}`);
       }
       return {
           
    successtrue
       
    };
    } catch (
    e) {
       return {
           
    successfalse,
           
    failReasone.message,
       };
    }
    }
    Проверим соединение. Перейдем на страницу модуля, в блоке Настройки телефонии нажмем на кнопку Проверить соединение. При корректной настройке, появится модальное окно со следующим содержанием:

    [​IMG]

    В коде можно также задать свой текст выводимого сообщения при ошибке в параметрах Error.

    Сопоставление пользователей ELMA и телефонии

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

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

    В левом списке можно настроить вывод пользователей с нужными параметрами и в нужном формате, например, код оператора указать в скобках и т.д.

    [​IMG]

    Код метода для сопоставления пользователей:
    Код:
    
    // данные пользователя телефонии РТУ
    interface VoipUser {
    // id пользователя
    idstring;
    // номер телефона
    phoneNumberstring;
    // почта
    emailstring;
    // ФИО
    namestring;
    // логин
    sipLoginstring;
    // пароль
    sipPasswordstring;
    }

    // получить список пользователей IP-телефонии (используется для сопоставления пользователей по нажатию кнопки "Настроить" на странице модуля)
    async function VoipGetMembers(): Promise<VoipMember[]> {
    let all_voip_users VoipUser[] = [];

    let all_users: {
       
    idstring;
       
    labelstring;
    }[] = [];

    if (!Namespace.
    params.data.domains_list || !Namespace.params.data.domains_list.length) {
       return [];
    }

    for (
    let one_row of Namespace.params.data.domains_list) {
       if (
    one_row.domain.length 0) {
           
    let login_and_pwd btoa(`${one_row.domain}:${one_row.password}`);
           
    let response_get_users await fetch(`${Namespace.params.data.api_endpoint}/users`, {
             
    method'GET',
             
    headers: {
                 
    'Authorization': `Basic ${login_and_pwd}`
             }
           });

           if (!
    response_get_users.ok) {
             
    await System.cache.setItem("testapi", `запрос получения пользователей ${response_get_users.status}${response_get_users.statusText}`, 300000);
             throw new 
    Error(`received error response get users ${response_get_users.status}${response_get_users.statusText}`);
           }

           const 
    result await response_get_users.json();

           const 
    users = <VoipUser[]>(result.list);

           
    let voip_user users.map(user => ({
             
    id: `${user.phoneNumber}${one_row.domain_digital_code}`,
             
    label: `${one_row.domain} ${user.name} (${user.phoneNumber})`,
           }));

           
    all_users all_users.concat(voip_user);
           
    all_voip_users all_voip_users.concat(users);
       }
    }

    await System.cache.setItem("testapi", `${JSON.stringify(all_voip_users)}`, 300000);

    let all_emails all_voip_users.map(=> x.email);

    let group_operators await System.userGroups.search().where(=> x.__name.eq("Операторы")).size(10000).first();

    if (!
    group_operators) {
       return 
    all_users;
    }

    let users_in_operators await group_operators.users(010000);
    if (!
    users_in_operators || !users_in_operators.length) {
       return 
    all_users;
    }

    let users_emails_in_operators users_in_operators.filter(=> x.data.email !== undefined).map(=> x.data.email!);
    let create_elma_users await System.users.search().where((xy) => y.and(x.email.in(users_emails_in_operators), x.email.in(all_emails))).size(10000).all();
    let operators await Namespace.params.fields.operators_app.app.search().where(=> x.email.in(all_emails)).size(10000).all();

    if (!
    create_elma_users) {
       return 
    all_users;
    }

    for (
    let one_user of create_elma_users) {
       
    let operator operators.find(=> x.data.email == one_user.data.email);
       if (!
    operator) {
           break;
       }

       
    let domain operator.data.domain;
       if (!
    domain) {
           break;
       }

       
    let domain_row = Namespace.params.data.domains_list.find(=> x.domain == domain);
       if (!
    domain_row) {
           break;
       }

       
    let pwd domain_row.password;

       if (!
    pwd) {
           break;
       }

       
    let login_and_pwd btoa(`${domain}:${pwd}`);

       
    let voip_user all_voip_users.find(=> x.email == one_user.data.email);
       if (!
    voip_user) {
           break;
       }

       
    let response_create_user await fetch(`${Namespace.params.data.api_endpoint}/user`, {
           
    method'POST',
           
    headers: {
             
    'Authorization': `Basic ${login_and_pwd}`,
             
    'Content-Type''application/json'
           
    },
           
    bodyJSON.stringify({
                 
    "id"voip_user.phoneNumber voip_user.phoneNumber "",
                 
    "phoneNumber"voip_user.phoneNumber voip_user.phoneNumber "",
                 
    "email"voip_user.email voip_user.email "",
                 
    "name"voip_user.name voip_user.name "",
                 
    "sipLogin"voip_user.sipLogin voip_user.sipLogin "",
                 
    "sipPassword"voip_user.sipPassword voip_user.sipPassword ""
             
    })
       });

       if (!
    response_create_user.ok) {
           throw new 
    Error(`received error response create user ${response_create_user.status}${response_create_user.statusText}`);
       }
    }


    let delete_elma_users await System.users.search().where((xy) =>
       
    y.or(
           
    y.not(x.email.in(users_emails_in_operators)),
           
    x.__status.eq(UserStatus.Blocked)
       )
    ).
    size(10000).all();


    for (
    let one_user of delete_elma_users) {
       
    let operator operators.find(=> x.data.email == one_user.data.email);
       if (!
    operator) {
           break;
       }

       
    let domain operator?.data.domain;
       if (!
    domain) {
           break;
       }

       
    let domain_row = Namespace.params.data.domains_list.find(=> x.domain == domain);

       
    let pwd domain_row?.password;
       
    let login_and_pwd btoa(`${domain}:${pwd}`);
       
    let voip_user all_voip_users.find(=> x.email == one_user.data.email);

       if (!
    voip_user || !voip_user.id) {
           break;
       }

    let response_delete_user await fetch(`${Namespace.params.data.api_endpoint}/user`, {
       
    method'DELETE',
       
    headers: {
           
    'Authorization': `Basic ${login_and_pwd}`
       },
       
    bodyJSON.stringify({
           
    "id"voip_user.id voip_user.id ""
       
    })
    });

       if (!
    response_delete_user.ok) {
           throw new 
    Error(`received error response delete user ${response_delete_user.status}${response_delete_user.statusText}`);
       }
       
    await operator.delete()
    }

    return 
    all_users;
    }
    Помимо функции возврата массива пользователей платформе для корректного их использования далее в телефонии необходимо воспользоваться функциями создания/обновления пользователей, а также удаления. Согласно ТЗ, у Заказчика первоначально у пользователей телефонии пустой id, id в то же время является признаком того, что пользователь стал оператором, именно поэтому его нужно заполнить, сделав вызов из ELMA365. Удаление пользователей же означает, что пользователь перестал быть оператором.

    Создание/обновление пользователей применяется к пользователям, отвечающим обоим правилам:

    1. Пользователь ELMA365 входит в группу Операторы SD
    2. Входит в массив пользователей РТУ
    Удаление пользователей применяется к следующим пользователям:
    1. Пользователь должен быть заблокирован или не входить в группу Операторов SD
    2. В элементе списка у него должно быть заполнено поле Id
    Также при удалении пользователя удаляется элемент приложения Операторы, соответствующий этому сотруднику (соответствие определяется по email).

    Далее во 2 части статьи рассмотрим настройку звонков: https://community.elma365.com/ru/threads/3159/
    Последнее редактирование: 6 авг 2024