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. Можно обратиться к ним.