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

Пользователи АТС могут совершать звонки друг другу и на реальные телефонные номера, отдельная группа пользователей (Операторы) могут также принимать входящие звонки, приходящие на АТС с обычных телефонов (об этом далее) или по 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-софтфон.