понедельник, 7 мая 2012 г.

Имитация поведения стационарного телефона в Lync

Поведением Lync по умолчанию является прием звонков вне зависимости от статуса абонента (кроме DND). Сигнал занятости линии отправляется только в случае ручного отклонения звонка или отсуствии подключенного клиента. При "миграции" со стационарных телефонов на Lync это ставит большинство пользователей в замешательство.
Enterprise решением будет покупка BusyOnBusy (3$\каждого пользователя Lync в пуле).
Если позволяют познания в линке, то можно сэкономить и использовать MSPL скрипт от добрых дядей. Я скрипт немного поправил под свои нужды, советую почитать оригинал. Недостаток - нет возможности индивидуального включения\выключения (была в оригинале) и "возможные" проблемы при использовании совместно с RGS\Attendant (на тестах проблем не обнаружилось, но кто знает...).
Скрипт подключается командой на каждом одном FE, файл со скриптом должен находиться на каждом(т.к. загрузка происходит локально):
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>

7 комментариев:

  1. а ты проверял на практике данный скрипт?

    ОтветитьУдалить
    Ответы
    1. Скрипт тестировал на Lync Standard, планирую запустить в Enterprise редакции на 2х серверах.
      Скрипт начал работать несколько неожиданно в закомментированном куске кода, которым я думал обойти работу с RGS. Судя по всему логика RGS самостоятельно правильно отрабатывает логику звонка.

      Удалить
    2. я пытался прикрутить (правда оригинальный скрипт) ничего у меня на первый взгляд не вышло.... но думаю ещё буду пробовать. у тебя скрипт идет 4-м по порядку с чем это связано и насколько критична очередность в случае если установлены только стандартные приложения?

      у тебя отличный блог!

      Удалить
    3. Спасибо. Пост поправил.
      Подсмотрел в оригинале, что звонок проходит по всем стандартным приложениям в порядке приоритета, посмотреть можно через Get-CsServerApplication. Если скрипт стоит слишком низко, то звонок маршрутизируется до того, как доходит до скрипта. На сколько я понял ничего страшного в установке скрипта слишком высоко нет - маршрутизация просто пойдет дальше, хотя я могу и ошибаться, опыта соответствующей разработки нет.

      Как только потестирую работу с RGS и допишу соответствующий кусок, пост дополню. Сам пост писал из-за того, что платить 3$/юзер за бизнес аналог - это слишком дорого (когда искал первый раз бесплатного аналога просто не было :( ), вдруг кому пригодится.

      Удалить
    4. скрипт зараза у меня не работает :-( ни оригинальный, ни твой

      Удалить
  2. слушай, а у меня есть ещё потребность в реализации возможности доставки сообщений всем пользователям. пробовал расширить максимальный размер группы в линке со 100 до 150 человек - но получилась лажа. теперь у меня идея сделать "бота" на который пишешь и сообщение доставляется всем кто заведен в линке. сам писать под линк не умею, может у тебя есть какие то соображения по этому поводу (может где видел в инете нечто подобное)?

    ОтветитьУдалить
    Ответы
    1. Не умею, в основном пользуюсь гуглом и копипастой при программировании на неизвестных мне языках. На StackOverflow обсуждают куски C#+UCMA, там можно найти материала.

      Вот эти ребята http://www.profit-ug.ru/ написали очень неплохого бота для Lync - умеет делать табель рабочего времени и интегрируется как автоответчик SCSM. Можно обратиться к ним.

      Удалить