Pull to refresh
69.07
Voximplant
Облачная платформа голосовой и видеотелефонии

IP АТС в облаке своими руками за 10 минут

Reading time 8 min
Views 72K
Многие из читателей Хабра знакомы с современными IP АТС в облаке, такими как Манго, Oktell, Октолайн и другими. Все они предлагают различные функции и тарифные планы, чтобы удовлетворить самых разных клиентов и соответствовать их требованиям, но всегда есть ряд тех кто все равно выбирает Asterisk, так как хочется иметь возможности по кастомизации и интеграции, которые кроме как Asterisk мало какая система способна предложить. В дополнение к самому Asteriskу еще потребуется человек, который будет способен все настроить и его поддерживать, отдельное подключение к оператору связи по SIP или через VoIP-шлюз и так далее. Когда мы начали создавать свою облачную платформу для разработки коммуникационных приложений VoxImplant, то, конечно же, знали, что одним из популярных сценариев ее использования будет IP АТС, поэтому реализовали весь необходимый для этого функционал. В отличие от случая с Asterisk человеку, который решит сделать свою IP АТС на базе VoxImplant, понадобится лишь знание Javascript, ознакомление с этой статьей и свободных 10-15 минут, чтобы получить на выходе первую рабочую версию АТС, которую потом в дальнейшем можно будет интегрировать со своими сервисами и кастомизировать в соответствии со своими требованиями. Подробнее об этом под катом.

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

Пользователи АТС могут совершать звонки друг другу и на реальные телефонные номера, отдельная группа пользователей (Операторы) могут также принимать входящие звонки, приходящие на АТС с обычных телефонов (об этом далее) или по SIP, соответственно они могут переводить вызовы на обычных пользователей и друг другу при необходимости. SIP-телефон — софтфон или железный телефон, с поддержкой SIP, Web SDK/Mobile SDK — клиентское приложение для браузера или смартфона, сделанное с помощью соответствующего SDK от VoxImplant.

Итак, для создания IP АТС нам потребуется бесплатный аккаунт разработчика VoxImplant, который можно получить тут.
После создания и активации аккаунта можно зайти в панель управления, где преимущественно и будет происходить вся дальнейшая наша работа. Для начала нам потребуется создать приложение (Application), а в нем пользователей (Users), которые будут соответствовать пользователям и операторам АТС, при создании пользователя можно выбрать будет ли у него отдельный лицевой счет VoxImplant (Separate account balance) или же при его звонках сумма будет списываться с общего счета аккаунта (по умолчанию) — пока ничего не меняем, делаем по умолчанию. ВАЖНО: Используйте для логин/username 3х значные цифровые коды (101, 102, 103 и т.д.) — в своих скриптах мы будем исходить из такого формата. Теперь можно приступать непосредственно к созданию функционала АТС, серверные приложения VoxImplant представляют собой набор сценариев, по которым обрабатываются звонки, проходящие через платформу. Сценарии эти пишутся на обычном Javascript, в котором доступны несколько неймспейсов и классов для работы с функциями VoxImplant (подробнее можно посмотреть по ссылке). Сценарии создаются и редактируются в разделе «Сценарии». Всего у нас будет предусмотрено 3 типа сценариев обработки звонков: для входящих, для исходящих и для звонков между пользователями, назовем их PBX in, PBX out и PBX local соответственно. Начнем с самого простого PBX local:

VoxEngine.forwardCallToUser();

«И это все?» — спросите вы :) Да, это все, так как forwardCallToUser — это одна из helper-функций, которую мы написали для ускорения и облегчения создания приложений. По сути, за этой функцией скрывается кусочек Javascript-кода, а-ля:

VoxEngine.addEventListener(AppEvents.CallAlerting, function(e) {
    var newCall = this.callUser(e.destination, e.callerid, e.displayName);
    VoxEngine.easyProcess(e.call, newCall);
});

Где easyProcess — еще одна из helper-функций, более подробно со всеми функциями можно ознакомиться в документации к VoxEngine, так как у нас сейчас не стоит задача изучить VoxImplant досконально, то продолжим без углубления в нюансы. Следующий скрипт (PBX out) отвечает за отправку исходящих звонков на обычные телефонные номера:

VoxEngine.addEventListener(AppEvents.CallAlerting, function (e) {
    var call2 = VoxEngine.callPSTN(e.destination.substring(1), "74957893798");
    VoxEngine.easyProcess(e.call, call2);
});

В данном коде мы используем функцию callPSTN, чтобы перенаправить звонок в телефонную сеть с указанием Caller ID — 74957893798, который нужно предварительно авторизовать в разделе Settings -> Caller IDs, чтобы АОН при звонке показывал что это мы звоним. Для первого параметра просто откидывает префикс из 1 цифры (позже мы настроим эту цифру во время привязки сценариев к приложению), которая сигнализирует что звонок надо пропустить в ТфОП, то есть номер вводится в виде 974952200022, после того как 9 откинули останется 74952200022 — обычный московский номер. Тут тоже все достаточно просто, теперь переходим к самому интересному сценарию — обработка входящих звонков (PBX in), я буду добавлять «мясо» по частям, чтобы было понятнее:

// Переменные доступные сессии
var callerid,
    displayName;

// При поступлении звонка на платформу всегда первым происходит событие AppEvents.CallAlerting
VoxEngine.addEventListener(AppEvents.CallAlerting, function(e) {
    // запомним Caller ID
    callerid = e.callerid;
    // и displayName - актуально для SIP
    displayName = e.displayName;
    // Включим обработчик тоновых сигналов (чтобы можно было донабрать добавочный сразу)
    e.call.handleTones(true);
    // Ответим со стороны платформы на входящий звонок, чтобы установить связь между звонящим и платформой
    e.call.answer();
});

Соединение с платформой мы разрешили, теперь нужно заняться обработкой звонка, внутри предыдущего листенера добавляем обработку события соединения звонка:

// Переменные для указания текущей таймзоны, и рабочих часов офиса для разных дней недели
var GMT_offset = 4,
    workingHours = [
        [0, 0], // Sun
        [10, 19], // Mon
        [10, 19], // Tue
        [10, 19], // Wed
        [10, 19], // Thu
        [10, 19], // Fri
        [0, 0] // Sat
    ],
    nonWorkingHours = false,
    workingHoursGreetingURL = 'http://yourdomain.com/ivr.mp3', //<=== ПОМЕНЯТЬ НА РЕАЛЬНЫЙ URL с MP3-файлом приветствия (рабочее время)
    nonWorkingHoursGreetingURL = 'http://yourdomain.com/ivr_nwh.mp3';  //<=== ПОМЕНЯТЬ НА РЕАЛЬНЫЙ URL с MP3-файлом приветствия (нерабочее время)

e.call.addEventListener(CallEvents.Connected, function(e) {
    // См. день/час для текущей таймзоны, указанной в GMT_offset (GMT+4 для Москвы)
     var d = new Date(new Date().getTime() + GMT_offset * 3600 * 1000),
        day = d.getUTCDay(),
        hour = d.getUTCHours();
    Logger.write("Day: " + day + " Hour: " + hour);
    if (hour >= workingHours[day][0] && hour < workingHours[day][1]) {
        /**
        * Звонок в рабочие часы
        * проигрываем стандартное приветствие
        */
       e.call.startPlayback(workingHoursGreetingURL, false);
       e.call.record();
   } else {
       /** 
        * Звонок в нерабочее время
        * проигрываем специальное приветствие
        */
        nonWorkingHours = true;
        e.call.startPlayback(nonWorkingHoursGreetingURL, false);
    }
});

Обработчик на соединение звонка повесили, теперь нужно повесить обработчик на разъединение:

e.call.addEventListener(CallEvents.Disconnected, function(e) {
    // Завершаем сессию
    VoxEngine.terminate();
});

И обработчик завершения проигрывания нашего приветствия:

// Таймаут чтобы дать возможность ввести добавочный после проигрывания приветствия
var TIMEOUT = 3000,
    operatorTimer;

e.call.addEventListener(CallEvents.PlaybackFinished, function(e) {

    // Если звонок пришел в рабочее время, то даем какое-то время чтобы ввести добавочный, прежде чем соединить с оператором
    if (!nonWorkingHours) {
        operatorTimer = setTimeout(function() {
            forwardCallToOperator(e.call);
        }, TIMEOUT);
    } else VoxEngine.terminate(); // если звонок в нерабочее время - просто завершаем сессию 
});

Вы, наверное, заметили функцию forwardCallToOperator, мы к ней вернемся сразу как подключим обработку нажатий кнопок на телефоне для ввода добавочного номера. Ранее мы уже включили обработчик с помощью вызова e.call.handleTones(true), теперь надо его объявить:

// Переменная для хранения введенных цифр
var input = '';

e.call.addEventListener(CallEvents.ToneReceived, function(e) {
    // При нажатии клавиши останавливаем проигрывание нашего приветствия
    e.call.stopPlayback();
    // добавляем введенную цифру
    input += e.tone;
    if (input.length == 3) {
        // исходим из того что пользователям были выданы 3х значные логины и отправляем вызов на пользователя
        forwardCallToExtension(e.call, input);
    }
});

Ну а теперь пришло время самых интересных функций — forwardCallToExtension и forwardCallToOperator:

var operators = ['101', '102', '103'], // <== Здесь указываем пользователей АТС, которые будут операторами - на приеме входящих
    operatorCalls = {},
    nOperatorCalls = 0,
    activeOperatorCall;

function forwardCallToExtension(call, ext) {
    clearTimeout(operatorTimer);
    // выключаем обработку нажатий на кнопки телефона
    call.handleTones(false);
    // проигрываем гудки (длительность и тональность принятые в России)
    call.playProgressTone("RU");
    // звоним пользователю, соответствующему введенному добавочному номеру
    var call2 = VoxEngine.callUser(ext, callerid, displayName);
    // Вешаем обработчики
    call2.addEventListener(CallEvents.Failed, VoxEngine.terminate);
    call2.addEventListener(CallEvents.Connected, function(e) {
        // если дозвонились - соединяем звонящего с пользователем АТС
        VoxEngine.sendMediaBetween(call, call2);
    });
    call2.addEventListener(CallEvents.Disconnected, VoxEngine.terminate);
}

function forwardCallToOperator(call) {
    // выключаем обработку нажатий на кнопки телефона
    call.handleTones(false);
    nOperatorCalls = 0;
    // проигрываем гудки (длительность и тональность принятые в России)
    call.playProgressTone("RU");
    // звоним сразу на всех операторов параллельно, соединяем с первым свободным, вешаем обработчики.
    for (var i in operators) {
        var j = operators[i];
        nOperatorCalls++;
        operatorCalls[j] = VoxEngine.callUser(j, callerid, displayName);
        operatorCalls[j].addEventListener(CallEvents.Failed, function(e) {
            if (typeof activeOperatorCall == "undefined") {
                delete operatorCalls[e.call.number()];
                nOperatorCalls--;
                if (nOperatorCalls == 0) {
                    call.hangup();
                }
            }
        });

        operatorCalls[j].addEventListener(CallEvents.Connected, function(e) {
            delete operatorCalls[e.call.number()];
            activeOperatorCall = e.call;
            VoxEngine.sendMediaBetween(call, e.call);
            activeOperatorCall.addEventListener(CallEvents.Disconnected, VoxEngine.terminate);
            for (var i in operatorCalls) {
                operatorCalls[i].hangup();
            }
            operatorCalls = {};
        });
    }
}

Все готово, объединяем наши части и получаем полный скрипт. Дело осталось за малым — заставить сценарии работать с помощью правил. Для этого перейдем в раздел «Роутинг» и создадим 3 новых правила: local, out, in
При создании правила local, учитывая что имена пользователям вы задали в виде 101, 102, 103 и т.д. в поле Маска указываем 1[0-9]{2} и прикрепляем сценарий PBX local, чтобы назначить его в качестве активного, если подключенные к нашей АТС пользователи решат позвонить на внутренний номер своего коллеги.


Аналогично для правила out в поле Pattern указываем 9[0-9]+ (помните ту самую 9, которую мы отпиливаем с помощью e.destination.substring(1) в методе callPSTN) и назначаем сценарий PBX out. Сохраняем.
И последнее правило in: в Pattern указываем что-то в духе (74957893798|100) и прикрепляем PBX in. Сохраняем. 74957893798 – это просто пример, который вы можете подключить к своему приложению в разделе Phone numbers. А 100 позволяет позвонить на нашу АТС по SIP, используя URL вида sip:100@appname.accountname.voximplant.com, где appname — название вашего приложения, которые вы указали во время его создания, а accountname — имя аккаунта VoxImplant, которое вы указали при регистрации. Если у вас уже есть купленный номер, поддерживающий форвардинг по SIP, то вы сможете его подключить к АТС, например, направив вызовы на тот SIP URI (sip:100@appname.accountname.voximplant.com), который мы сделали для входящих звонков по SIP.


Теперь наша АТС готова к подключению SIP-телефонов (или клиентских приложений на базе VoxImplant SDK), приему и обработке вызовов, локальным звонкам между пользователями АТС и исходящим звонками на реальные номера.

P.S. Готовые скрипты из статьи выложены на GitHub. Готовый веб-телефон на базе VoxImplant Web SDK можно найти по адресу, не забудьте заменить accountname и appname на свои названия. Ну и X-lite/Bria никто не отменял, если нужен более-менее вменяемый SIP-софтфон.
Tags:
Hubs:
+19
Comments 70
Comments Comments 70

Articles

Information

Website
www.voximplant.com
Registered
Founded
Employees
101–200 employees
Location
Россия