Enterprise решением будет покупка BusyOnBusy (3$\каждого пользователя Lync в пуле).
Если позволяют познания в линке, то можно сэкономить и использовать MSPL скрипт от добрых дядей. Я скрипт немного поправил под свои нужды, советую почитать оригинал. Недостаток - нет возможности индивидуального включения\выключения (была в оригинале) и "возможные" проблемы при использовании совместно с RGS\Attendant (на тестах проблем не обнаружилось, но кто знает...).
Скрипт подключается командой на
New-CsServerApplication -Identity "Service:Registrar:msk10lync01.contoso/BusyBusy2" -Uri "http://www.meldingtechnology.com/busybusy" -ScriptName "C:\scripts\busybusy2.am" -Enabled $true -Critical $false -Priority 4
Uri должно совпадать с указанным в скрипте. Работу скрипта можно приостановить из Web интерфейса Lync, отключив application в настойка сервера в топологии (надо поправить нормально как подсмотрю, где это в вебинтнрфейсе)
Звонок проходит по всем стандартным приложениям в порядке приоритета, посмотреть можно через Get-CsServerApplication. Если скрипт стоит слишком низко, то звонок маршрутизируется до того, как доходит до обработки в скрипте. Приоритет 4 выбрал по своему хотению.
На сколько я понял ничего страшного в установке скрипта слишком высоко нет - маршрутизация просто пойдет дальше, хотя я могу и ошибаться, опыта соответствующей разработки нет.
Upd: добавил кусочек для обработки преобразвания p2p в конференц звонок
Upd2: Почему-то в Enterprise версии пока не сменишь имя файла и appUri файл по факту не перечитывается. Меня это повергает в уныние
Сам скрипт:
<?xml version="1.0" ?>
<lc:applicationManifest
lc:appUri="http://www.meldingtechnology.com/busybusy"
xmlns:lc="http://schemas.microsoft.com/lcs/2006/05">
<lc:allowRegistrationBeforeUserServices action="true" />
<lc:requestFilter methodNames="INVITE"
strictRoute="true"
registrarGenerated="true"
domainSupported="true" />
<lc:responseFilter reasonCodes="NONE" />
<lc:proxyByDefault action="false" />
<lc:scriptOnly />
<lc:splScript>
<![CDATA[
// Проверим тело сообщения. Если это SDP и договариваемся о звуке (media - audio)
// возвращаем true
function contentHasSDPAudio(content) {
// SDP format is strict enough that the following check is a valid
// way to determine if the offer includes audio.
if (ContainsString(content,"\nm=audio ", false) ||
ContainsString(content,"\rm=audio ", false)) {
//Log( "Debug", false, "***BusyBusy***: found m=audio" );
return true;
}
return false;
}
Log( "Debugr", false, "***BusyBusy***: We have a request - ", sipRequest.Method );
//Если это обновление сессии, ничего не делаем
foreach ( sessionExpires in GetHeaderValues( "Session-Expires" ) ) {
if ( ContainsString( sessionExpires, "refresher", true ) ) {
Log( "Debugr", false, "***BusyBusy***: skipped; This is a session refreshing invite" );
return;
}
}
//
// Получаем значение заголовка To:
//
toUri = GetUri(sipRequest.To);
Log( "Debugr", false, "***BusyBusy***: toUri - ", toUri );
//Log( "Event", false, "***BusyBusy***: RequestUri ", sipRequest.RequestUri);
// Игнорируем запросы от голосовой почты, иначе может случиться зацикливание.
// ***** Replace with your Exchange UM account
/*
if (ContainsString(sipRequest.RequestUri, "sip:MTLync@mtex.MeldingTech.Com", false)) {
Log("Debug", false, "***BusyBusy***: ignoring Voice mail request" );
ProxyRequest();
return;
}
*/
//
// Звонок из конференции
//
if (ContainsString(toUri,"opaque=app:conf:focus", false)) {
Log("Event", false, "***BusyBusy***: Not processing. Conference call");
ProxyRequest();
return;
}
if (ContainsString(toUri,"opaque=app:conf:audio-video", false)) {
Log("Event", false, "***BusyBusy***: Not processing. Conference call");
ProxyRequest();
return;
}
if (sipRequest.StandardMethod == StandardMethod.Invite) {
// Проверяем наличие тела у пакета и запроса на аудио
hasBody = false;
hasAudio = false;
foreach (header in GetHeaderValues (StandardHeader.ContentType)) {
// Если есть заголовок content-type, то у сообщения есть тело
hasBody = true;
if (IndexOfString (header, "multipart/", true) == 0) {
//Log( "Debugr", false, "***BusyBusy***: Found multipart body. Content-Type:", header );
i = 0;
while (i<MultiPartItem.Count && BindMultiPartBodyItem(i)) {
if (ContainsString(MultiPartItem.ContentType, "application/sdp", true)) {
//Log("Debugr", false, "***BusyBusy***: Found SDP content-type in Multipart item count: ", i);
if (contentHasSDPAudio(MultiPartItem.Content)) {
//Log("Debug", false, "***BusyBusy***: content has audio" );
hasAudio = true;
break;
}
}
i=i+1;
}
}
}
if ( hasAudio ) {
Log("Debugr", false, "***BusyBusy***: this is an audio call" );
}
else if (!hasBody) {
//Пакет INVITE без тела. Предполагаем, что это голос.
Log("Debugr", false, "***BusyBusy***: content has no body, implied to be an audio call" );
}
else {
//Не звуковой INVITE, не обрабатываем
Log("Debugr", false, "***BusyBusy***: this is not an audio call!" );
ProxyRequest();
return;
}
}
//Проверяем откуда пришел звонок(ПО)
/*
//этот кусок кода отрубает звонок на группу дозвона, хотя должно быть наоборот
//Внутренняя логика звонков на RGS должна отрабатывать звонок правильно.
agentString = GetHeaderValues(StandardHeader.UserAgent);
if (ContainsString(agentString, "Response_Group_Service", false)) {
Log("Event", false, "***BusyBusy***: Not processing. Call to RGS");
ProxyRequest();
return;
}
if (ContainsString(agentString, "Microsoft Lync 2010 Attendant", false)) {
Log("Event", false, "***BusyBusy***: Not processing. Call to Attendant");
ProxyRequest();
return;
}
//Проверяем звонок на переадресацию
referredByString = GetHeaderValues("Referred-By");
if (LengthString(referredByString) > 0) {
Log("Event", false, "***BusyBusy***: Not processing. Call is transferred from ",referredByString);
return;
}
*/
totalEndpoints = 0;
anyEndpointBusy = false;
//проходимся по подключенным клиентам, проверяем их занятость
foreach (dbEndpoint in QueryEndpoints(toUri)) {
totalEndpoints = totalEndpoints + 1;
Log( "Debugr", false, "***BusyBusy***: endpoint.EPID - ", dbEndpoint.EPID );
Log( "Debugr", false, "***BusyBusy***: endpoint.ContactInfo - ", dbEndpoint.ContactInfo );
//Log( "Debugr", false, "***BusyBusy***: endpoint.Instance - ", dbEndpoint.Instance );
publication = QueryCategory(toUri, 2, "state", dbEndpoint.Instance);
//Log( "Debugr", false, "***BusyBusy***: State - ", publication );
if (IndexOfString(publication, "on-the-phone") >= 0) {
Log( "Debugr", false, "***BusyBusy***: endpoint in a call change to busy state" );
anyEndpointBusy = true;
break;
}
else {
Log( "Debugr", false, "***BusyBusy***: endpoint not in call stay in free state" );
}
}
//Log( "Debugr", false, "***BusyBusy***: found ", totalEndpoints, " endpoint(s)" );
// Если один из клиентов занят, выдаем отбой или перенаправляем на голосовую почту.
// If any point is busy respond with voice mail of busy signal
if (anyEndpointBusy) {
if (RequestTarget.Aor != "BENOTIFY") {
// Check if user is enabled for UM
/*
userProperties = QueryCategory(toUri, 1, "userProperties", 0);
Log( "Debugr", false, "***BusyBusy***: User Properties - ", userProperties );
if (ContainsString(userProperties , "1</exumEnabled>", false)) {
Log( "Debugr", false, "***BusyBusy***: Redirecting to voice mail for ", toUri);
toVoiceMail = Concatenate( toUri, ";opaque=app:voicemail");
ProxyRequest(toVoiceMail);
return;
}
else {
*/
Respond( 486, "Busy here" );
Log( "Debugr", false, "***BusyBusy***: Busy response given for ", toUri);
//log a request which was replied with busy signal
Log( "Event" , true, "***BusyBusy***: Busy response given for ", toUri);
return;
// }
}
}
Log( "Debugr", false, "***BusyBusy***: finished script.. no action taken");
ProxyRequest();
return;
]]>
</lc:splScript>
</lc:applicationManifest>
а ты проверял на практике данный скрипт?
ОтветитьУдалитьСкрипт тестировал на Lync Standard, планирую запустить в Enterprise редакции на 2х серверах.
УдалитьСкрипт начал работать несколько неожиданно в закомментированном куске кода, которым я думал обойти работу с RGS. Судя по всему логика RGS самостоятельно правильно отрабатывает логику звонка.
я пытался прикрутить (правда оригинальный скрипт) ничего у меня на первый взгляд не вышло.... но думаю ещё буду пробовать. у тебя скрипт идет 4-м по порядку с чем это связано и насколько критична очередность в случае если установлены только стандартные приложения?
Удалитьу тебя отличный блог!
Спасибо. Пост поправил.
УдалитьПодсмотрел в оригинале, что звонок проходит по всем стандартным приложениям в порядке приоритета, посмотреть можно через Get-CsServerApplication. Если скрипт стоит слишком низко, то звонок маршрутизируется до того, как доходит до скрипта. На сколько я понял ничего страшного в установке скрипта слишком высоко нет - маршрутизация просто пойдет дальше, хотя я могу и ошибаться, опыта соответствующей разработки нет.
Как только потестирую работу с RGS и допишу соответствующий кусок, пост дополню. Сам пост писал из-за того, что платить 3$/юзер за бизнес аналог - это слишком дорого (когда искал первый раз бесплатного аналога просто не было :( ), вдруг кому пригодится.
скрипт зараза у меня не работает :-( ни оригинальный, ни твой
Удалитьслушай, а у меня есть ещё потребность в реализации возможности доставки сообщений всем пользователям. пробовал расширить максимальный размер группы в линке со 100 до 150 человек - но получилась лажа. теперь у меня идея сделать "бота" на который пишешь и сообщение доставляется всем кто заведен в линке. сам писать под линк не умею, может у тебя есть какие то соображения по этому поводу (может где видел в инете нечто подобное)?
ОтветитьУдалитьНе умею, в основном пользуюсь гуглом и копипастой при программировании на неизвестных мне языках. На StackOverflow обсуждают куски C#+UCMA, там можно найти материала.
УдалитьВот эти ребята http://www.profit-ug.ru/ написали очень неплохого бота для Lync - умеет делать табель рабочего времени и интегрируется как автоответчик SCSM. Можно обратиться к ним.