Microsoft Lync and Skype for Business have a rich set of .NET APIs which make it easy to extend the platform and integrate it with other applications. This blog helps explain how to use those APIs.

How to publish presence using UCMA v2.0 and have a clever Communicator status

Posted: March 4th, 2009 | Author: | Filed under: OCS Development, UCMA 2.0 | No Comments »

One of the many useful features of the second version of the Unified Communications Managed API is its capability to publish presence through managed code. Using the methods of the LocalOwnerPresence instance attached to each endpoint, you can publish presence information in the form of XML in the five built-in presence categories provided by Office Communications Server: note, contactCard, calendarData, services, and state. You can also, if you choose, control which access control containers the presence information is published into. (The containers determine who gets to see the presence information; these are the groups that you assign people to by right-clicking them in Communicator and going to the Change Level of Access submenu.) If you like to live on the edge, you can even create your own custom presence categories, which you could use to publish information like your geographical location, subjects you can take customer calls about, the names of your pets,etc. You need to execute some stored procedures on the database used by OCS in order to actually put the categories into the running.

So,a few days ago I decided that “Available” didn’t quite do justice to my attitude; it seemed a bit mild and uninspired, especially with the dreary, foggy weather in Chicago. I wanted my Communicator availability to show more enthusiasm about how extremely available I am to come up with new uses for UCMA 2.0. With just a handful of lines of code, I published a bit of XML to my state presence category, causing me to show up in Communicator like this:

image

In order to publish the presence information, I had to start up a CollaborationPlatform, then establish a UserEndpoint (the type of endpoint you use in UCMA 2.0 if you want your code to act on behalf of a particular user), then publish the presence, and then terminate the endpoint and shut down the platform.

The block of XML that I used, stored in a constant, looks like this. The {0} represents a numeric availability code; the {1} represents the custom activity string that I want to publish.

private const string STATE_XML_FORMAT = 
    "<state xmlns="http://schemas.microsoft.com/2006/09/sip/state" " +  
    "xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" manual=" + 
    ""true" xsi:type="userState">{0}" + 
    "{1}";

I also stuck the value for the “Available” level of availability (the green circle in Communicator) in a constant.

private const int AVAILABLE = 3500;

I’ll make a quick digression at this point to explain what’s going on with these availability codes.

There are four types of “state” that OCS aggregates together to determine what presence will show up next to your name in Communicator. Those four are user state (what you set yourself manually), machine state (how recently you did something on the computer, phone, device, or whatever), phone state (whether you are in a phone call or conference call), and calendar state (what’s on your calendar for the current time).

Ranges of availability codes correspond to different colors for the presence dot; for example, the 3xxx range gets you a green dot. 12xxx gets you a half-green, half-yellow dot. The availability codes have slightly different meanings for the different types of state. In machine state, 3500 means you are actively using the device. In calendar state, 3500 means that you are free according to your calendar at the current time.

You’ll notice in the XML above that the type of state we are publishing is user state. I’ve chosen to publish my state as “available,” using an availability code of 3500. I could have used any of the following:

  • 3500 (Available)
  • 6500 (Busy)
  • 9500 (Do Not Disturb)
  • 12500 (Be Right Back)
  • 15500 (Away)
  • 18500 (Offline)

The first step in publishing the presence information is starting up the CollaborationPlatform. To do this, I have to instantiate a ServerPlatformSettings object and pass it into the CollaborationPlatform constructor. Then I call BeginStartup on the new CollaborationPlatform.

public void Run()
{
    Console.Write("Enter your SIP URI: ");
    _sipUri = Console.ReadLine();
 
    Console.Write("Enter the activity string you want to publish: ");
    _activityString = Console.ReadLine();
 
    ServerPlatformSettings settings = new ServerPlatformSettings(
            APP_USER_AGENT,
            LOCALHOST,
            PORT,
            GRUU,
            GetLocalCertificate()
        );
 
    _platform = new CollaborationPlatform(settings);
 
    _platform.BeginStartup(StartupCompleted, null);
}

Like most UCMA operations, starting up the CollaborationPlatform is asynchronous, so I’ve provided a callback delegate as the first parameter of the BeginStartup method. (The second parameter is an optional state object, which will be available in the AsyncState property of the IAsyncResult that I will get as a parameter of the callback delegate. I don’t use this here, since everything I need is stored in instance variables.)

You can force UCMA methods like this to execute synchronously (i.e., block the thread until they finish) by chaining together the begin and end methods, like so:

_platform.EndStartup(_platform.BeginStartup(StartupCompleted, null));

This is usually not a good idea, though, unless you are just testing something and want to keep it very simple. The best practice is to supply a callback delegate that will execute on a new thread whenever the UCMA operation completes. This is especially important if you want your application to be at all scalable.

The next step, in the callback method for BeginStartup, is to establish a UserEndpoint. First, though, we have to call EndStartup on the CollaborationPlatform. It’s important always to call the corresponding end method for any UCMA begin method you call. Usually you would want to wrap it in a try-catch block, because if an exception occurred during the asynchronous execution of the method, it will get thrown in your thread when you call the end method.

private void StartupCompleted(IAsyncResult result)
{
    _platform.EndStartup(result);
 
    Console.WriteLine("Started up platform.");
 
    UserEndpointSettings endpointSettings = new UserEndpointSettings(
        _sipUri,
        OCS_SERVER,
        TLS_PORT
    );
 
    _endpoint = new UserEndpoint(_platform, endpointSettings);
 
    _endpoint.BeginEstablish(EstablishCompleted, null);
}

This is similar to the process for creating the CollaborationPlatform; we need a UserEndpointSettings object which we then pass into the UserEndpoint constructor. Once the endpoint is instantiated, we need to call BeginEstablish on it.

Once the endpoint finishes establishing, we’re ready to publish the presence.

The BeginPublishPresence method takes an array of PresenceCategory objects. PresenceCategory is an abstract class, with two derivatives: CustomPresenceCategory and PresenceCategoryWIthMetaData. The latter is what we would need to use if we wanted to specify things like which presence containers the presence information should go into. We would also have to use it if we were publishing presence for an ApplicationEndpoint.

In this case, we can use a CustomPresenceCategory to wrap the presence information. The constructor takes the category name (in this case, “state”) and the XML document.

private void EstablishCompleted(IAsyncResult result)
{
    _endpoint.EndEstablish(result);
 
    Console.WriteLine("Established endpoint.");
 
    string stateXml = String.Format(
        STATE_XML_FORMAT, 
        AVAILABLE, 
        _activityString
    );
 
    CustomPresenceCategory customCategory =
        new CustomPresenceCategory("state", stateXml);
 
    PresenceCategory[] categoriesToPublish = 
        new PresenceCategory[] { customCategory };
 
    _endpoint.LocalOwnerPresence.BeginPublishPresence(
        categoriesToPublish, 
        PublishPresenceCompleted,
        null
    );
}

Once the publish operation completes, we can clean up after ourselves by terminating the endpoint and shutting down the platform. The code to do this is really simple.

private void PublishPresenceCompleted(IAsyncResult result)
{
    _endpoint.LocalOwnerPresence.EndPublishPresence(result);
 
    Console.WriteLine("Published presence.");
 
    _endpoint.BeginTerminate(TerminateCompleted, null);
}
 
private void TerminateCompleted(IAsyncResult result)
{
    _endpoint.EndTerminate(result);
 
    Console.WriteLine("Terminated endpoint.");
 
    _platform.BeginShutdown(ShutdownCompleted, null);    
}
 
private void ShutdownCompleted(IAsyncResult result)
{
    _platform.EndShutdown(result);
 
    Console.WriteLine("Shut down platform.");
}

If you run this code (make sure you are running it as an administrator, or you’ll get a TLS exception) it should publish the activity message you choose so that it shows up in Communicator beside your name.

Here is the complete code for my CustomActivityPublisher class.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Rtc.Collaboration;
using Microsoft.Rtc.Collaboration.Presence;
using System.Security.Cryptography.X509Certificates;
using System.Net;
 
namespace PresenceSample
{
    public class CustomActivityPublisher
    {
        private const string STATE_XML_FORMAT = 
            "<state xmlns="http://schemas.microsoft.com/2006/09/sip/state"" +  
            " xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" manual=" + 
            ""true" xsi:type="userState">{0}" + 
            "{1}";
        private const int AVAILABLE = 3500;
 
        private const string APP_USER_AGENT = ...
        private const string LOCALHOST = ...
        private const int PORT = ...
        private const string GRUU = ...
 
        private const string OCS_SERVER = ...
        private const int TLS_PORT = 5061;
 
        private string _sipUri;
        private string _activityString;
        private CollaborationPlatform _platform;
        private UserEndpoint _endpoint;
 
        public void Run()
        {
            Console.Write("Enter your SIP URI: ");
            _sipUri = Console.ReadLine();
 
            Console.Write("Enter the new activity string: ");
            _activityString = Console.ReadLine();
 
            ServerPlatformSettings settings = 
                new ServerPlatformSettings(
                    APP_USER_AGENT,
                    LOCALHOST,
                    PORT,
                    GRUU,
                    GetLocalCertificate()
                );
 
            _platform = new CollaborationPlatform(settings);
 
            _platform.BeginStartup(StartupCompleted, null);
        }
 
        private void StartupCompleted(IAsyncResult result)
        {
            _platform.EndStartup(result);
 
            Console.WriteLine("Started up platform.");
 
            UserEndpointSettings endpointSettings = 
                new UserEndpointSettings(
                    _sipUri,
                    OCS_SERVER,
                    TLS_PORT
                );
 
            _endpoint = new UserEndpoint(_platform, endpointSettings);
 
            _endpoint.BeginEstablish(EstablishCompleted, null);
        }
 
        private void EstablishCompleted(IAsyncResult result)
        {
            _endpoint.EndEstablish(result);
 
            Console.WriteLine("Established endpoint.");
 
            string stateXml = String.Format(
                STATE_XML_FORMAT, 
                AVAILABLE, 
                _activityString
            );
 
            CustomPresenceCategory customCategory =
                new CustomPresenceCategory("state", stateXml);
 
            PresenceCategory[] categoriesToPublish = 
                new PresenceCategory[] { customCategory };
 
            _endpoint.LocalOwnerPresence.BeginPublishPresence(
                categoriesToPublish, 
                PublishPresenceCompleted,
                null
            );
        }
 
        private void PublishPresenceCompleted(IAsyncResult result)
        {
            _endpoint.LocalOwnerPresence.EndPublishPresence(result);
 
            Console.WriteLine("Published presence.");
 
            _endpoint.BeginTerminate(TerminateCompleted, null);
        }
 
        private void TerminateCompleted(IAsyncResult result)
        {
            _endpoint.EndTerminate(result);
 
            Console.WriteLine("Terminated endpoint.");
 
            _platform.BeginShutdown(ShutdownCompleted, null);    
        }
 
        private void ShutdownCompleted(IAsyncResult result)
        {
            _platform.EndShutdown(result);
 
            Console.WriteLine("Shut down platform.");
        }
 
        private static X509Certificate2 GetLocalCertificate()
        {
            X509Store store = 
                new X509Store(StoreLocation.LocalMachine);
            store.Open(OpenFlags.ReadOnly);
 
            X509Certificate2Collection certificates = 
                store.Certificates;
 
            foreach (X509Certificate2 certificate in certificates)
            {
                if (certificate.SubjectName.Name.ToLower().Contains(
                    Dns.GetHostEntry("localhost").HostName.ToLower()) 
                    && certificate.HasPrivateKey)
                {
                    return certificate;
                }
            }
 
            return null;
        }
    }
}



Leave a Reply

  • Note: Comment moderation is in use because of excessive spam. Your comment may not appear immediately.

  •