...

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

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

  1. vyimova

    vyimova Участник

    В предыдущей части статьи (https://community.elma365.com/ru/threads/3158/) рассмотрели как предварительно настроить модуль, проверить соединение и сопоставить пользователей. Теперь перейдем непосредственно к самим звонкам.

    Исходящие звонки

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

    Т.к. в функцию VoipGenerateCall не передается поле типа Пользователь, реализуем виджет для определения текущего пользователя при инициации звонка, затем запишем данные в кэш стенда, где ключом будет – Номер телефона, а значением – Id пользователя, инициировавшего звонок.

    На форме просмотра элемента, откуда предполагается инициировать звонки (и где есть поле типа Телефон) разместим Виджет-код. Предварительно на одном из виджетов формы, поверх которого находится поле типа Телефон, например, виджет Содержимое модального окна, добавим класс data_contact. Скрипт виджет-кода:
    Код:
    
    <script>
    function 
    getPhone() {
         
    let myTimeout window.setTimeout(() => {
             const 
    data_contact document.querySelector('.data_contact');
             if (
    data_contact) {
                 
    data_contact.addEventListener("click"async (e) => {
                     if (
    e.target.closest('.phone-menu')) {
                         
    let phone e.target.closest('.phone-menu').parentElement.querySelector('a').textContent;
                         
    phone phone.trimStart();
                         
    await <%= Scripts%>.saveData(phone);
                     }
                 });
                 
    window.clearTimeout(myTimeout);
             }
         }, 
    300);
    }
    getPhone();
    </script>
    В этом скрипте мы, отловив запуск исходящего звонка, вызываем функцию saveData, куда передаем в качестве параметра значение номера телефона, на который был выполнен звонок.

    Также в клиентском сценарии формы напишем следующий скрипт:
    Код:
    
    async function saveData(phone_numberstring): Promise<void> {
    try {
       if (!
    phone_number) {
           return;
       }

       
    let current_user await System.users.getCurrentUser();
       if (!
    current_user) {
           return;
       }

       
    let current_user_id current_user.id;
       if (!
    current_user_id) {
           return;
       }

       
    await System.cache.setItem(phone_numbercurrent_user_id300000);
    }
    catch (
    e) {
       
    console.log(e);
    }
    }
    Подобный скрипт можно вынести на все формы, откуда планируется совершать исходящие звонки, а также в рут-виджет в случае, если звонки планируется совершать со страниц приложений, из виджета пропущенных входящих звонков и т.д.

    Помимо виджета для генерации исходящих звонков со стенда необходимо ввести следующий код в Методы API:
    Код:
    
    // сгенерировать звонок
    async function VoipGenerateCall(srcPhonestringdstPhonestring): Promise<void> {
    if (!
    srcPhone) {
       return;
    }

    srcPhone srcPhone.includes("_") ? (srcPhone).substring(0, (srcPhone).indexOf("_")) : srcPhone;

    if (
    dstPhone && dstPhone.length && dstPhone[0] === '+') {
       
    dstPhone dstPhone.substring(2);
       
    dstPhone "8" dstPhone;
    }

    let current_user_id await System.cache.getItem(dstPhone);
    if (!
    current_user_id) {
       return;
    }

    let current_user await System.users.search().where(=> x.__id.eq(current_user_id)).size(10000).first();
    if (!
    current_user) {
       return;
    }

    let operator await Namespace.params.fields.operators_app.app.search().where(=> x.operator.eq(current_user!)).size(10000).first();
    if (!
    operator) {
       return;
    }

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

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

    let pwd domain_row.password;
    if (!
    pwd) {
       return;
    }

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

    let response_call await fetch(`${Namespace.params.data.api_endpoint}/call`, {
       
    method'POST',
       
    headers: {
           
    'Authorization': `Basic ${login_and_pwd}`,
           
    'Content-Type''application/json'
       
    },
       
    bodyJSON.stringify({
           
    "user"srcPhone,
           
    "destination"dstPhone
       
    })
    });

    await System.cache.setItem("testapi", `${srcPhone} ${dstPhone} ${response_call.statusText} ${new Date().toString()}`, 300000);

    if (!
    response_call.ok) {
       throw new 
    Error(`received error response call ${response_call.status}${response_call.statusText}`);
    }
    }
    Затем сгенерируем звонок. Если наш текущий пользователь сопоставлен какому-либо пользователю телефонии, то у нас появится возможность выполнять исходящие звонки, а также у всех полей типа Телефон появится справа иконка стрелочки вниз, нажав на которую появится выпадающий список.

    [​IMG]

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

    [​IMG]

    Получение Webhook URL

    Для получения ссылки на Webhook, нужно добавить следующий метод в Методы API в модуле:
    Код:
    
    // получить ссылку на Webhook
    async function VoipOnWebhookUpdated(webhookUrlstring): Promise<void> {
    Namespace.
    storage.setItem("webhookUrl_rtu"webhookUrl);
    }
    Данный метод позволяет получать в параметрах актуальную строку с Webhook URL и записывать ее в кэш, например, модуля. А затем в нужном месте получать данную ссылку с помощью метода storage getItem, например, на форме модуля.

    Входящие звонки

    У телефонии РТУ есть особенность относительно входящих звонков. Она заключается в том, что запросы входящего звонка присылаются не на Webhook URL напрямую, а на указанную ссылку и добавленный эндпоинт. В случае начала и завершения звонка /call (POST и PUT).

    [​IMG]

    Чтобы можно было принимать входящие запросы от РТУ, необходимо в Методах API создать метод /call (POST и PUT). Его цель состоит в том, чтобы получать и обрабатывать данные, полученные из запроса. В этом методе также вызовем метод fetchToVoipParseWebhookRequest, в который передадим body запроса. Далее в методе fetchToVoipParseWebhookRequest необходимо выполнить запрос на Webhook URL, передав body и метод – POST или PUT.

    Код данных методов:
    Код:
    
    // входящий звонок
    async function call(reqFetchRequest): Promise<HttpResponse void> {
    try {
       
    await System.cache.setItem("testapi"typeof req.body === 'string' "call " req.body "undefined"300000);

       if (
    typeof req.body !== 'string') {
           throw new 
    Error('Expected request body to be string')
       }
       if (!
    req.method) {
           throw new 
    Error('Expected request method')
       }

       const 
    req_dataNotifyRequest JSON.parse(req.body);

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

       
    req_data.user = `${req_data.user}${domain_row.domain_digital_code}`;

       const 
    body = {
           
    datareq_data,
           
    methodreq.method,
           
    endpoint'call'
       
    }

       
    // _phone.has проверяет точное совпадение, т.е. номер записан именно через 8... или +7...
       
    let contact await Namespace.params.fields.contacts_app.app.search().where(=> f._phone.has(req_data.source)).first();

       if (!
    contact) {
           return;
       }

       
    fetchToVoipParseWebhookRequest(body);

       return new 
    HttpResponse().status(200)
    } catch (
    e) {
       return new 
    HttpResponse()
           .
    status(400)
           .
    content(JSON.stringify({
             
    errore.message ?? 'internal error',
           }))
    }
    }

    // запрос к Webhook Телефонии ELMA
    async function fetchToVoipParseWebhookRequest(bodyWebhookRequest): Promise<void> {
    await System.cache.setItem("testapi", `fetchToVoipParseWebhookRequest ${JSON.stringify(body)}`, 300000);
    try {
       const 
    webhookUrl await Namespace.storage.getItem("webhookUrl_rtu");
       if (
    webhookUrl) {
           
    let resp await fetch(webhookUrl, {
             
    method'POST',
             
    bodyJSON.stringify(body),
           });

           if (!
    resp.ok) {
             
    await System.cache.setItem("testapi"resp.statusText300000);
           }
       } else {
           
    await System.cache.setItem("testapi", `Webhook Url не определен (выполните его сохранение)`, 300000);
           throw { 
    message'Webhook Url не определен (выполните его сохранение)' }
       }
    }
    catch (
    e) {
       
    await System.cache.setItem("testapi", `${e}`, 300000);
    }
    }
    После чего можно реализовать метод обработки входящих сообщений и добавление логики при старте дозвона, ответе оператора на звонок, завершении звонка или же при пропущенном звонке.

    Зададим в коде следующие правила:

    1. При старте дозвона у пользователя должно появляться всплывающее уведомление, при клике на которое открывается карточка контакта, от которого поступил звонок (т.е. у которого указан номер, с которого выполняется звонок)
      [​IMG]
    2. При завершении звонка в карточке контакта, от которого поступил звонок, в Ленту событий добавляется сообщение о совершенном звонке с аудиозаписью этого звонка
      [​IMG]
    3. В случае пропущенного звонка – у пользователя появляется иконка виджета с пропущенными звонками или же инкрементируется счетчик пропущенных звонков у уже отображенной иконки. Также при клике на иконку открывается сам виджет со списком пропущенных значков, где есть новая запись о пропущенном звонке
      [​IMG]
      [​IMG]
    Если перейти в карточку контакта, там как и в случае с завершенным звонком появится сообщение в Ленте событий, однако не будет аудиозаписи разговора, вместо нее будет только надпись Нет ответа.

    Для реализация подобной логики нужно ввести следующий код метода для обработки входящих звонков:
    Код:
    
    // обработать запрос от провайдера IP-телефонии
    async function VoipParseWebhookRequest(requestFetchRequest): Promise<VoipWebhookParseResult> {
    try {
       if (
    typeof request.body !== 'string') {
           throw new 
    Error('Expected request body to be string')
       }

       
    let eventVoipWebhookRequest undefined;
       
    let callRecordVoipCallRecord undefined;
       
    let responseHttpResponse undefined;

       const 
    reqAny WebhookRequest JSON.parse(request.body);

       
    await System.cache.setItem("testapi", `VoipParseWebhookRequest ${request.body}`, 300000);

       if (
    reqAny.endpoint === 'notify') {
           const 
    req = <WebhookRequest>JSON.parse(request.body);
           const 
    dstPhone req.data.user ?? '';

           
    await System.cache.setItem("testapi", `VoipParseWebhookRequest звонок отвечен notify ${JSON.stringify(req)}`, 300000);

           
    event = {
             
    eventVoipWebhookEvent.NotifyAnswer,
             
    directionVoipCallDirection.In,
             
    dstPhonedstPhone,
             
    srcPhonereq.data.source,
             
    dispositionVoipCallDisposition.Answered,
           }

           
    response = new HttpResponse()
             .
    status(200)
             .
    content(JSON.stringify({
                 
    code200,
                 
    reason'OK'
             
    }))

           return {
             
    eventevent,
             
    callRecordcallRecord,
             
    responseresponse,
           };
       }
       else {
           const 
    req = <WebhookRequest>JSON.parse(request.body);
           const 
    dstPhone req.data.user ?? '';

           switch (
    req.method) {
             
    // пришел входящий звонок (в это время у менеджера должен начать звонить телефон)
             
    case "POST": {
                 
    await System.cache.setItem("testapi", `VoipParseWebhookRequest POST пришел звонок call ${JSON.stringify(req)}`, 300000);
                 
    event = {
                   
    eventVoipWebhookEvent.NotifyStart,
                   
    directionVoipCallDirection.In,
                   
    dstPhonedstPhone,
                   
    srcPhonereq.data.source,
                   
    dispositionVoipCallDisposition.Unknown,
                 }

                 
    response = new HttpResponse()
                   .
    status(200)
                   .
    content(JSON.stringify({
                       
    code200,
                       
    reason'OK'
                   
    }))
             } break;

             
    // уведомление о завершении вызова
             
    case "PUT": {
                 if (
    req.data.callRecord) {
                   
    await System.cache.setItem("testapi", `завершен звонок call ${JSON.stringify(req)}`, 300000);
                   
    event = {
                       
    eventVoipWebhookEvent.NotifyEnd,
                       
    directionVoipCallDirection.In,
                       
    dstPhonedstPhone,
                       
    srcPhonereq.data.source,
                       
    dispositionVoipCallDisposition.Answered,
                   }

                   
    // после успешного звонка в CRM отправляется запрос с данными о звонке и ссылкой на запись разговора.
                   // команда может быть использована для сохранения в данных ваших клиентов истории и записей входящих и исходящих звонков.
                   
    const cmd req.data;
                   
    callRecord = {
                       
    srcPhonecmd.source,
                       
    dstPhonedstPhone,
                       
    directionVoipCallDirection.In,
                       
    durationcmd.durationSeconds,
                       
    // данные из этого поля будут доступны в функции VoipGetCallLink
                       
    call: <RTUCallData>{
                         
    linkcmd.callRecord ? (cmd.callRecord).substring(0, (cmd.callRecord).indexOf("&")) : "",
                         
    idcmd.protocolConfId,
                       },
                       
    dispositionVoipCallDisposition.Answered,
                   }
                 }
                 else {
                   
    await System.cache.setItem("testapi", `пропущен звонок call ${JSON.stringify(req)} ${dstPhone}`, 300000);
                   
    event = {
                       
    eventVoipWebhookEvent.NotifyEnd,
                       
    directionVoipCallDirection.In,
                       
    dstPhonedstPhone,
                       
    srcPhonereq.data.source,
                       
    dispositionVoipCallDisposition.Cancel,
                   }
                   
    // после успешного звонка в CRM отправляется запрос с данными о звонке и ссылкой на запись разговора.
                   // команда может быть использована для сохранения в данных ваших клиентов истории и записей входящих и исходящих звонков.
                   
    const cmd req.data;
                   
    callRecord = {
                       
    srcPhonecmd.source,
                       
    dstPhonedstPhone,
                       
    directionVoipCallDirection.In,
                       
    durationcmd.durationSeconds,
                       
    // данные из этого поля будут доступны в функции VoipGetCallLink
                       
    call: <RTUCallData>{
                         
    link"",
                         
    idcmd.protocolConfId,
                       },
                       
    dispositionVoipCallDisposition.Cancel,
                   }
                 }
                 
    response = new HttpResponse()
                   .
    status(200)
                   .
    content(JSON.stringify({
                       
    code200,
                       
    reason'OK'
                   
    }))
             } break;
             default: throw new 
    Error(`Unknown method type "${req.method}"`)
           }
           return {
             
    eventevent,
             
    callRecordcallRecord,
             
    responseresponse,
           };
       }
    } catch (
    e) {
       return {
           
    response: new HttpResponse()
             .
    status(400)
             .
    content(JSON.stringify({
                 
    errore.message ?? 'internal error',
             }))
       };
    }
    }
    Таким образом, можно реализовать основные функции телефонии для взаимодействия с IP-телефонией РТУ.
    Последнее редактирование: 6 авг 2024