Click to Rate and Give Feedback
MSDN
MSDN Library
Real-time Communications (RTC) Client Technical Articles
Implementing Automated Agents (Bots) using the RTC Client API
 

Robert Osborne
Microsoft Corporation

May 2004

Applies to:
    Microsoft® Real-Time Communications Client API version 1.2
    Microsoft Office Live Communications Server 2003
    Instant messaging applications

Summary: The Microsoft Real-Time Communications (RTC) Client application programming interface (API) version 1.2 can be used for end user desktop applications, or for automated agents to provide functionality for a large number of users. This article discusses automated agents, and by providing an example, demonstrates the techniques you can utilize to develop such applications. (36 printed pages)

Download RTC_InformationAgent.EXE.


Contents

Introduction
Application Scenarios
Example Implementation
Deploying a Bot with Live Communications Server
Conclusion
Related Links

Introduction

An automated agent, or bot (short for robot), is an application that functions on behalf of a user. Implementation of such bots has been common on the Web to provide additional functionality and carry out complex and time consuming tasks. For example, Web search engines often use a spider bot, which is an automated program that searches the Internet for new Web documents and places information about them in a database that can be accessed by a search engine.

With the proliferation of instant messaging, organizations are utilizing bots as another means of providing end user functionality. Using instant messaging to communicate with bots has several advantages, such as the instant messaging infrastructure can provide authentication of the user as well as the bot.

This article covers the techniques required to use the RTC Client API in developing such a bot. It begins by describing the types of bot applications you can build by using the RTC Client API and then moves into considerations that you need to take into account when building bot applications. Next, the article details an example application and how it provides functionality for the end user. Finally, this article covers additional information that explains how to deploy a bot application in an organization.

Application Scenarios

There are two classes of bot applications: user interaction and user assistance.

  • User interaction class applications, which are the most common, have an instant messaging session with the user who explicitly set up the bot for a specific purpose.
  • User assistance class applications usually provide additional functionality to the user, but are usually added to an existing, ongoing session between two or more users.

The following are examples of such applications:

Distribution Lists or Chat Rooms

Instant messaging provides the ability for one user to communicate to another user. A user can add additional people into an existing instant messaging conversation by specifying who can be included within that conversation. For example, an employee might want to start an instant messaging conversation with a manager to discuss a certain problem.

However, it is possible that a user wants to start an instant messaging conversation to discuss a particular problem, but is unsure of who can help. For example, if the user has a question to ask through e-mail to a group of unknown people, the user can send the e-mail to a Distribution List (DL). This same concept can be used with instant messaging, if an application exists that can act as the DL and deliver the question to the members of that DL.

Figure 1. A user communicating with a distribution bot that forwards an instant message to members of the Distribution List

For example, Northwind Traders has created a DL called msofficeXperts in Active Directory® directory services containing people from the information technology (IT) department who are trained to troubleshoot any issues employees might have while using Microsoft Office. Bob is having trouble getting the presence status of his contacts within Microsoft Outlook®. He sends an instant message to the bot indicating that he wants to speak to an expert. The bot sets up a multiparty instant messaging session between Bob and all the members of msofficeXperts.

Legacy Application Interaction

Many corporations have a large amount of infrastructure and applications already in place, for example: purchase ordering systems, which are in some cases only accessible through terminals, and not Microsoft Windows® desktops. Providing a mechanism by which a user can interact with these systems through a Windows desktop can be a complicated deployment problem. By utilizing an existing desktop application like Windows Messenger, and deploying a bot, the deployment solution becomes much simpler. The automated agent intercepts the instant messages from the user and converts them into specific, legacy application commands. The agent then returns the relevant legacy application responses as instant messages to the user.

This can be achieved by allowing a user to use an instant messaging application by implementing an automated agent. The automated agent intercepts the instant messages from the user and converts them into specific, legacy application commands. It then returns the relevant, legacy application responses as instant messages to the user.

Figure 2. A user communicating with a legacy application bot that interacts with legacy applications

Translation Bots

The previous two examples illustrated bots that provided user interaction. A translation bot is an example of a bot that provides user assistance.

Today, it is more common for people of different languages to communicate with each other. This occurred with e-mail, and now it is occurring with instant messaging. Traditionally, this meant that one of the people had to be fluent in both languages, which is not always possible.

One solution to this problem is to provide a bot that is able to translate between the two languages. If it is added as an additional member within an ongoing instant messaging conversation, it can receive each user's text. Once received, it can then convert that text into the other language, and deliver it to the participants in the conversation.

Figure 3. A translation bot receiving and converting instant messages from one language to another

Example Implementation

The remainder of this white article discusses how to implement a bot. The application has two main purposes:

  • Distribution Lists
  • Chat Rooms

Even though the example application has two main purposes, its architecture and core implementation can be used as a template for other type of bots. By using this implementation as a guideline, other functionality that interacts with users or legacy applications in the relevant manner can be plugged in. Throughout the discussion of the example application, the locations where by different application logic can be added are highlighted.

Distribution Lists

In this application, a DL is defined as an explicit list created by any user. This list has a name (such as Product X Feature Team), and contains members. The name is determined by the user who creates the DL. The members are users who chose to become members of the DL. Creating and managing this DL is achieved by sending instant messages to a bot that has a generic identity, such as infoagent@northwindtraders.com.

Once the DL is created, any user (even those who are not a member of the DL) can send an instant message to the members of the DL. All members of the DL then receive this instant message. For example, when a user is having a problem with an e-mail server, the user can send an instant message to the DL that contains the organization's e-mail server support engineers.

The bot interacts with the user by accepting commands as instant messages. The commands are as follows:

  • /help lists Allows the user to see a list of the all the available commands related to managing distribution lists.
  • /CreateList [name] Allows the user to create a new DL with the name specified by [name].
  • /RemoveList [name] Allows the user who owns an existing DL, specified by [name], to delete that list.
  • /SetSecurity [listName] [level (0-2)] Allows the owner of a DL to set the security level between 0 and 2 for that list.

    0: Only the owner is allowed to perform operations.

    1: Only members of the list are allowed to perform operations.

    2: Anyone can perform an operation.

  • /JoinList [name] Allows users to add themselves to an existing DL specified by [name].
  • /LeaveList [name] Allows users to remove themselves from an existing DL specified by [name].
  • /Notify [name] [message] Broadcast text as specified by [message] to the members of the DL specified by [name]. Once the member receives the [message], that member is able to reply to all other members that have received that [message].
  • /Lists Show a list of all available DLs.
  • /MyLists Show a list of all the DLs that the user is a member of.
  • /ListInfo [name] Show the owner and members of a DL specified by [name].

Chat Rooms

Distribution Lists are great for providing broadcast type functionality to a set of unknown users. However, it does not solve all the functionality required to allow users to communicate with each other. For example, Internet Relay Chat (IRC) allows users to create a chat room about a specific topic. Other users can join or leave the chat room as well as receive the text typed by all members of that chat room.

The example application also allows for users to create, join, and leave ongoing multiparty chat conversations between unknown users regarding a certain topic. For example, stockbrokers may join a chat room related to a specific stock and discuss the highlights of the day.

The bot interacts with the user by accepting commands as instant messages. The commands are as follows:

  • /CreateChat [name] Allows the user to create a new chat room as specified by [name].
  • /EndChat [name] Allows the user to close an existing chat room as specified by [name].
  • /JoinChat [name] Allows the user to become a member of an ongoing chat room as specified by [name], and participate in the conversation.
  • /ListChats List all the available chat rooms.

Architecture

The example application is split into two key pieces of functionality. The first piece of functionality is common to any type of bot, such as one for a DL or a chat room, and can be used to develop additional, custom bots. The second piece of functionality provides the DL and chat room functionality. More descriptions of these pieces follow.

Common Functionality

The common pieces of code provide functionality that is required to:

  • initialize the bot.
  • ensure that the bot has connected to the Session Initiation Protocol (SIP) server correctly.
  • supply credentials that the SIP server uses to authenticate the bot.
  • receive events such as incoming instant messages and handle them accordingly.

The code that is used to initialize the bot is main.cpp. The important code is contained within the main function, as shown in the following section.

Initializing RTC Client API

When the application is first started, it needs to initialize an instance of the RTC Client API that it uses to interact with the bot user. As part of this initialization, the RTC Client API object CLSID_RTCClient is created, and the event sink of the bot application that receives events from the RTC Client API is supplied. In addition, the application instructs the RTC Client API to only allow for incoming sessions that are for instant messaging, and to disallow audio and video sessions. The following sample shows how to initialize the RTC Client API:

   IRTCClient2 *pClient = NULL;
   HRESULT hr;
   
   hr = ::CoCreateInstance(CLSID_RTCClient, 
      NULL, 
      CLSCTX_INPROC_SERVER, 
      IID_IRTCClient2, 
      (LPVOID *) &pClient);
   
   if(FAILED(hr))
   {
      FXLog("Could not create the RTCClient object. Error is %x.\n", hr);
         return -1;
   }
   
   // When initializing, Disable the Media Stack and
// Enable Server Class mode
   hr = pClient->InitializeEx( RTCIF_DISABLE_MEDIA );
      if (FAILED(hr))
   {
      FXLog("InitializeEx() failed. Error is %x.\n", hr);
         return -1;
   }
   
   pEventSink = new CEventSink();
      if (!pEventSink)
   {
      FXLog("Not enough memory to allocate EventSink().
Terminating...\n");
      return -1;
   }
   
   hr = pEventSink->Attach(pClient);
   if (FAILED(hr))
   {
      FXLog("CEventSink->Attach() failed. Error is %x.\n", hr);
      return -1;
   }

   FXLog("Loading the Subscription List from persistent store...\n");
   BSTR storeFile = ::SysAllocString(L"subs.xml");
   pEventSink->DeSerialize(storeFile);
   ::SysFreeString(storeFile);


   pClient->put_EventFilter(
      RTCEF_CLIENT                  |
      RTCEF_REGISTRATION_STATE_CHANGE   |
      RTCEF_SESSION_STATE_CHANGE      |
      RTCEF_SESSION_OPERATION_COMPLETE   |
      RTCEF_PARTICIPANT_STATE_CHANGE      |
      RTCEF_MESSAGING               |
      RTCEF_BUDDY                  |
      RTCEF_BUDDY2                  |
      RTCEF_PROFILE                  |
      RTCEF_WATCHER                  |
      RTCEF_WATCHER2                  |
      RTCEF_ROAMING
   );

   // Set the listen mode for RTC client
   // RTCLM_BOTH opens the standard SIP port 5060, as well as
   // a dynamic port.
    
   hr = pClient->put_AllowedPorts(RTCTR_TCP, RTCLM_BOTH);
   hr = pClient->put_AllowedPorts(RTCTR_UDP, RTCLM_BOTH);

   // Set the Answer Mode for Various sessions
   pClient->put_AnswerMode(RTCST_PC_TO_PC, RTCAM_OFFER_SESSION_EVENT);
   pClient->put_AnswerMode(RTCST_IM, RTCAM_OFFER_SESSION_EVENT);
   pClient->put_AnswerMode(RTCST_MULTIPARTY_IM,
RTCAM_OFFER_SESSION_EVENT);
   pClient->put_AnswerMode(RTCST_APPLICATION, RTCAM_OFFER_SESSION_EVENT);
   pClient->put_AnswerMode(RTCST_PC_TO_PHONE, RTCAM_AUTOMATICALLY_REJECT);
   pClient->put_AnswerMode(RTCST_PHONE_TO_PHONE,
RTCAM_AUTOMATICALLY_REJECT);

Connecting to the SIP Server

It is important to connect to the SIP Server as it ensures that the bot is made available to everyone. There are several stages involved in connecting to the SIP Server.

The first stage includes the bot application to determine which SIP Server to connect to, as well as the relevant identity and account information. This information is supplied to the bot application through the command line parameters. Once this information is obtained, the application requests the RTC Client API to create a profile object. This is created asynchronously, and when the RTC_GET_PROFILE event is received, the application enables this profile. The following sample shows how to request a profile:

// GetProfile First--The event handler will call EnablePrenceEx and then
// EnableProfileEx.
   pProvisioning->GetProfile(username, password, updatedURI, servername, 
lTransport, 0);

The following sample shows how to receive a profile and log onto a server.

//////////////////////////////////////////////////////////////////////
// Handle Profile events received from RTC Client
// Enable the Profile when we first receive it
//////////////////////////////////////////////////////////////////////
HRESULT CEventSink::HandleProfileEvent(IDispatch *pEvent)
{

   FX_ENTRY("CEventSink::HandleProfileEvent");

   IRTCProfileEvent2 *pPE = NULL;
   IRTCProfile2  *pProfile = NULL;
   RTC_PROFILE_EVENT_TYPE enType;
   HRESULT hr;

   hr = pEvent->QueryInterface(IID_IRTCProfileEvent2, (void **)&pPE);
      if (FAILED(hr))
   {
      FXLog("QueryInterface failed for IID_IRTCProfileEvent2. 
[hr= %x]", hr);
      return hr;
   }
   
   pPE->get_Profile((IRTCProfile **) &pProfile);
   pPE->get_EventType(&enType);
   pPE->Release();

FXLog("Got Profile Event Type = %s\n", enType == RTCPFET_PROFILE_GET ?
 "GET" : "UPDATE");
   
   if (enType == RTCPFET_PROFILE_GET)
   {
pProfile->put_AllowedAuth(RTCAU_NTLM | RTCAU_KERBEROS |
RTCAU_USE_LOGON_CRED);
      FXLog("Auth mode is set to NTLM, KERBEROS, and 
Use Logon Credentials\n");
   
      //Enable Local storage
      IRTCClientPresence2 *pPresence = NULL;
      hr = m_pClient->QueryInterface(IID_IRTCClientPresence2, 
(LPVOID *)&pPresence);   
      if (FAILED(hr))
      {
         FXLog("QueryInterface failed for IID_IRTCClientPresence2. 
[hr= %x]", hr);
         return hr;
      }

      VARIANT v;
      ::VariantInit(&v);
      v.bstrVal = ::SysAllocString(L"infoagent_watchers.xml");
      v.vt = VT_BSTR;
      pPresence->EnablePresenceEx(pProfile, v, 0);
      pPresence->Release();

      FXLog("Enabled Presence\n");
      ::SysFreeString(v.bstrVal);
   
      // Enable Profile
      IRTCClientProvisioning2 *pProvisioning = NULL;
      hr = m_pClient->QueryInterface(IID_IRTCClientProvisioning2, 
(LPVOID *)&pProvisioning);   
      if (FAILED(hr))
      {
         FXLog("QueryInterface failed for IID_IRTCClientProvisioning2.
[hr= %x]", hr);
         return hr;
      }

      hr = pProvisioning->EnableProfileEx(pProfile, RTCRF_REGISTER_ALL,
RTCRMF_PRESENCE_ROAMING | RTCRMF_WATCHER_ROAMING);
      if (FAILED(hr))
      {
         FXLog("EnableProfileEx failed. [hr= %x]", hr);
         return hr;
      }
   
      FXLog("Profile Enabled\n");   
      pProvisioning->Release();
   }

   pProfile->Release();

   return S_OK;

}

Setting Presence Information

Now that the bot is connected to the server, by default, presence information is exposed on its behalf. To obtain the bot's presence, which either needs to be allowed or blocked, any user can now add the bot as a contact within the client application. By default, the presence state of the bot needs to be set as Online.

This specification of presence is achieved as follows:

   //We will handle all Watchers explicitly
   pPresence->put_OfferWatcherMode(RTCOWM_OFFER_WATCHER_EVENT);
   FXLog("Will Handle all watcher events\n");
   
   pPresence->SetLocalPresenceInfo(RTCXS_PRESENCE_ONLINE,NULL);
   FXLog("Set Presence Online\n");

Adding a Bot to a Contact List

In addition to setting presence, the following code contains an event that is triggered when a user adds the bot as a contact. This allows the user to see a bot's presence information, and allows the bot to send the user informational text as to how to use the bot. The following sample shows how to add a user's contact information to a bot:

//////////////////////////////////////////////////////////////////////
// Handle Watcher events received from RTC Client
// Accept any incoming watchers and send them a welcome message
//////////////////////////////////////////////////////////////////////
HRESULT CEventSink::HandleWatcherEvent(IDispatch *pEvent)
{
   FX_ENTRY("CEventSink::HandleWatcherEvent");
   
   IRTCWatcherEvent2 *pWEvent = NULL;
   IRTCWatcher2 *pWatcher = NULL;
   HRESULT hr;

   RTC_WATCHER_EVENT_TYPE enType;

   hr = pEvent->QueryInterface(IID_IRTCWatcherEvent2, (LPVOID *)&pWEvent);
   if (FAILED(hr))
   {
      FXLog("QueryInterface failed for IID_IRTCWatcher2. [hr= %x]", hr);
      return hr;
   }
   
   pWEvent->get_Watcher((IRTCWatcher**)&pWatcher);
   pWEvent->get_EventType(&enType);
   pWEvent->Release();

   //Get watcher Info
   RTC_WATCHER_STATE enState;
   RTC_ACE_SCOPE     enScope;
   BSTR bstrURI = NULL;
   BSTR bstrName = NULL;

   char* szWEventType[] = {"Add", "Remove", "Update", "Offering" };
   char* szWSState[] = { "Unknown", "Offering", "Allowed", "Blocked",
"Denied" };
   char* szACEScope[] = { "User", "Domain", "All"};

   pWatcher->get_PresentityURI(&bstrURI);
   pWatcher->get_Name(&bstrName);
   pWatcher->get_State(&enState);
   pWatcher->get_Scope(&enScope);
   
   FXLog("New Watcher: %S [%S] EventType= %s State=%s Scope=%s\n", 
            bstrName,
            bstrURI,
            szWEventType[enType],
            szWSState[enState],
            szACEScope[enScope]);

   if (m_pLogFile)
   {
      SYSTEMTIME tmStruct;
      GetLocalTime(&tmStruct);

      fprintf(m_pLogFile,"%2d-%2d-%4d %2d:%2d:%2d.%3d Watcher: %S [%S]
Type = %s\n",

   tmStruct.wMonth,tmStruct.wDay,tmStruct.wYear,tmStruct.wHour,
tmStruct.wMinute,tmStruct.wSecond,tmStruct.wMilliseconds,
   bstrName,bstrURI, szWEventType[enType]); 
   }

   // If it's an offering event, Allow the user and send them a welcome
// message. 

   if (enType == RTCWET_WATCHER_OFFERING)
   {
      pWatcher->put_State(RTCWS_ALLOWED);

      IRTCSession *pSession = NULL;
      IRTCParticipant *pPart = NULL;
   
      hr = m_pClient->CreateSession(RTCST_MULTIPARTY_IM, NULL, NULL, 0,
(IRTCSession**) &pSession);
      if (FAILED(hr))
      {
         FXLog("Failed to Create session [hr=0x%8x]\n", hr);
      }

      FXLog("Welcoming %S...\n", bstrURI);
      
      hr = pSession->AddParticipant(bstrURI, NULL, &pPart);
      if (FAILED(hr))
      {
         FXLog("Failed to Add Participant [hr=0x%8x]\n", hr);
      }

      BSTR welcomeMessage = ::SysAllocString(
         L"Welcome! I am your RTC Information Agent. Type \"/HELP\"
to see what I can do.");

      pSession->SendMessage(NULL, welcomeMessage, 1234);

   }

   if(bstrName)
   {
      ::SysFreeString(bstrName);
   }
   if(bstrURI)
   {
      ::SysFreeString(bstrURI);
   }

   pWatcher->Release();

   return S_OK;

}

Handling Instant Messaging Commands

The main function of the bot is to handle commands a user may specify through instant messages. When a user types an instant message, it sends a RTCE_MESSAGING event to the event handler, CEventSink::Event. Once this is received, the example code obtains all the information regarding the command, such as; the session, the user that sends the message, and the command and parameters entered by that user. The following example parses an incoming instant message command:

//////////////////////////////////////////////////////////////////////
// Handle Messaging events received from RTC Client
//
// - Dispatch the Commands to their various processors
// - Reply with default messages if no command processor available
//////////////////////////////////////////////////////////////////////
HRESULT CEventSink::HandleMessagingEvents(IDispatch *pEvent)
{
   FX_ENTRY("CEventSink::HandleMessagingEvents");

   HRESULT hr = S_OK;
   IRTCMessagingEvent* pME = NULL;
   IRTCParticipant* pPart = NULL;
   IRTCSession *pSession = NULL;
   BSTR bstrURI = NULL, bstrName = NULL;
   BSTR bstrMsg = NULL, bstrMsgHdr = NULL;
   
   hr = pEvent->QueryInterface(IID_IRTCMessagingEvent, (LPVOID *)&pME);
   if (FAILED(hr))
   {
      FXLog("Error: Could not QI for IID_IRTCMessageEvent\n");
      return E_FAIL;
   }

   hr = pME->get_Session(&pSession);
   if (FAILED(hr))
   {
      FXLog("Error: Could not get associated session\n");
      return E_FAIL;
   }

   pME->get_Participant(&pPart);
   if (FAILED(hr))
   {
      FXLog("Error: Could not get participant\n");
   }
   else
   {
      pPart->get_Name(&bstrName);
      pPart->get_UserURI(&bstrURI);
      pPart->Release();
   }

   pME->get_Message(&bstrMsg);
   hr = pME->get_MessageHeader(&bstrMsgHdr);
   if (FAILED(hr) && hr != RTC_E_NOT_EXIST)
   {
      FXLog("Error: GetMessageHeader failed [hr=0x%8x]\n", hr);
      bstrMsgHdr = NULL;
   }

        if(bstrMsg == NULL)
        {
           FXLog("MsgSkip: Got a NULL Message\n");
           goto singlePointOfExit;
       }

   // Increment the usage counter:
   m_lIncomingMessages++;
   
   // only msgs, not status
   RTC_MESSAGING_EVENT_TYPE enType;
   pME->get_EventType(&enType);
   
   if (enType == RTCMSET_MESSAGE)
   {
      if (m_pLogFile)
      {
         SYSTEMTIME tm;
         GetLocalTime(&tm);
         fprintf(m_pLogFile,
               "%2d-%2d-%4d %2d:%2d:%2d.%3d Got Msg from %S [%S]:\n",
               tm.wMonth,tm.wDay,tm.wYear,tm.wHour,tm.wMinute,tm.wSecond,
tm.wMilliseconds,
               bstrName,bstrURI); 
         fflush(m_pLogFile);
      }

      FXLog("Got Msg from %S [%S]\n header=%S\n%S\n",bstrName, bstrURI,
bstrMsgHdr, bstrMsg);

      // Handle any subscription commands
      char *szCommand = new char[wcslen(bstrMsg) + 3];
      if (!szCommand)
      {
         return E_OUTOFMEMORY;
      }
      CommandInfo pTI;
      hr = readCommand(bstrMsg, &pTI, ' ');

Once this information has been handled, specific functions are called for each command the bot can handle. For example, for the command to create a distribution list, /CreateList, the following code is executed (additional commands can be added by adding similar code):

else if (_strnicmp(szCommand, "CreateList", strlen("CreateList")) == 0)
   {
      ProcessCreateListCommand(&pTI, pSession, pPart);
   }

Distribution Lists Implementation

As previously mentioned, a DL is a mechanism that allows a user to send an instant message to a set of other users who have subscribed to the distribution list. Within the example application, the code behind the distribution list is contained within SubscriptionList.CPP and the CSubscriptionList object, which maps to a DL. This is accessed based on the instant message command previously described, as well as commands being entered by the user. The following section describes the code that is executed for each of the most often used commands:

/CreateList [name]

When a user issues a command to create a DL, the EventSink.CPP code first calls ProcessCreateListCommand, which creates an instance of the CSubscriptionList object. It sets the name specified by the user as being the owner of the DL. By default, the DL is set to allow only the owner to send a notification. The following sample in EventSink.CPP: shows how to create a Distribution List:

   for (int i = 0; i < pCommandInfo->numParams; i++)
   {
      // Does the list exist already?
      BOOL fExists = FALSE;
      
      for (itSubMap = subscriptionMap.begin(); itSubMap !=
subscriptionMap.end(); itSubMap++)
      {
         CSubscriptionList *subList = (CSubscriptionList*)
itSubMap->second;
         if (subList->equals(pCommandInfo->params[i]))
         {
            fExists = TRUE;
            break;
         }
      }

      if (fExists)
      {
         // Send error message and continue
         BSTR bstrMessage = NULL;
         wchar_t *wszMessage = new wchar_t[500 + 
wcslen(pCommandInfo->params[i])];
         if (!wszMessage)
         {
            return E_OUTOFMEMORY;
         }
         
         swprintf(wszMessage, L"Sorry, but the list %s already exists.
Use /Lists to see all available lists.",
pCommandInfo->params[i]);

         bstrMessage = ::SysAllocString(wszMessage);
         delete [] wszMessage;
         if (!bstrMessage)
            return E_OUTOFMEMORY;

         pSession->SendMessage(NULL, bstrMessage, 1234);

         ::SysFreeString(bstrMessage);
         continue;
      }

      // Create the new list
      CSubscriptionList *newSubscriptionList = 
new CSubscriptionList(pCommandInfo->params[i]);
      if (!newSubscriptionList)
      {
         return E_OUTOFMEMORY;
      }

      // Designate this user as the owner
      newSubscriptionList->SetOwner(bstrURI);

      // Add this user to his/her own list
      newSubscriptionList->AddSubscriber(bstrURI);
   
      // Add it to the map
      // Convert to char * from wchar_t *
      char *cTemp = new char[wcslen(pCommandInfo->params[i])+1];
      if (!cTemp)
      {
         return E_OUTOFMEMORY;
      }
      
      sprintf(cTemp, "%S", pCommandInfo->params[i]);

      subscriptionMap.insert(MAP_STR2SUBLIST::value_type(cTemp,
newSubscriptionList));

      pSession->SendMessage(NULL, L"Sure! (Y) List successfully 
created.", 1234);

      // Clean up
      delete [] cTemp;
   }

/JoinList [name]

When a user issues a command to join a Distribution List, the EventSink.CPP code first calls ProcessJoinListCommand which finds the relevant CSubscriptionList object, and adds the user as a subscriber. The following sample in EventSink.CPP: shows how to join a distribution list:

CSubscriptionList *subList = (CSubscriptionList*) itSubMap->second;

subList->AddSubscriber(bstrURI);

/Notify [name] [message]

When a user issues a command to send a message to a particular DL, EventSink.CPP calls ProcessNotifyCommand, which again finds the relevant CSubscriptionList object based on the name. It first informs the subscription list object of the message that needs to be sent and then calls the CSubscriptionList::CreateAndNotify member function.

The following code performs the major functionality of notifying all the users within the distribution list of the message that the original user specified. Within this member function, it enumerates through all the subscribers that have been originally added through the /JoinList command. For each member, it first adds the subscribers as participants within a multiparty instant messaging session. The following sample from SubscriptionList.CPP: shows how to add members of the DL to a session.

//////////////////////////////////////////////////////////////////////
// Initiates a Standard (Non-Quiet) Notification to a Distribution List
//////////////////////////////////////////////////////////////////////

HRESULT CSubscriptionList::CreateAndNotify(IRTCClient2 *pClient)
{
   FX_ENTRY("CSubscriptionList::CreateAndNotify");
   
   HRESULT hr = S_OK;
   int numPending = 0;
   RTC_SESSION_STATE enState;
   IRTCParticipant *pPart = NULL;
   SubscriberEntry *temp = m_pSubListHead;

   // Verify that the session is good... else destroy and recreate

   if (m_pSession != NULL)
   {
      m_pSession->get_State(&enState);
      if (enState != RTCSS_CONNECTED)
      {
         m_pSession->Release();
         m_pSession = NULL;
      }
   }
      
   // If no session exists, create one:
   
   if (m_pSession == NULL)
   {
      if (pClient == NULL)
      {
         return E_POINTER;
      }

      // Create the Multiparty IM Session
   
      hr = pClient->CreateSession(RTCST_MULTIPARTY_IM, NULL, m_pProfile,
0, (IRTCSession**) &m_pSession);
      if (FAILED(hr))
      {
         FXLog ("Failed to Create session [hr=0x%8x]\n", hr);
         goto singlePointOfExit;
      }

   }

   // Add the participants

   while (temp != NULL)
   {
      if (temp->value == NULL)
      {
         continue;
      }

      BSTR bstrURI = temp->value->GetSubscriberID();

      hr = m_pSession->AddParticipant(bstrURI, NULL, &pPart);
      if (FAILED(hr))
      {
         FXLog("Failed to Add Participant %S [hr=0x%8x]\n", bstrURI, hr);
      }
      else
      {
         // If AddParticipant succeeds, it's another person to wait for
// before sending the notify
         numPending++;
      }

      temp = temp->next;
   }

   // Set the global pending

   m_nPendingAdditions = numPending;

Adding participants into a session is an asynchronous mechanism, which results in another event RTCE_PARTICIPANT_STATE_CHANGE. When this is received within the EventSink.CPP event handler, it calls SendMessageCallBack. This call allows the subscription list object to monitor when all the participants, or subscribers, have successfully joined the multiparty instant messaging session. Once this has occurred, the code can then send the original instant message so that all subscribers can receive it, and respond if they want to.

//////////////////////////////////////////////////////////////////////
// Handle Participant Events received from RTC Client
// If the event is associated with a SubscriptionList (or other
// structure), do equals checks and call back
//////////////////////////////////////////////////////////////////////

HRESULT CEventSink::HandleParticipantEvents(IDispatch *pEvent)
{
   FX_ENTRY("CEventSink::HandleParticipantEvents");

   HRESULT hr = S_OK;

   IRTCSession *pSession = NULL;
   IRTCParticipantStateChangeEvent *pPSCE = NULL;
   IRTCParticipant *pPart = NULL;
   RTC_PARTICIPANT_STATE enState;
   MAP_STR2SUBLIST::iterator   itSubMap;
   
   hr = pEvent->QueryInterface(__uuidof(IRTCParticipantStateChangeEvent),
(LPVOID *) &pPSCE);
   if (FAILED(hr))
   {
      FXLog("QueryInterface failed for
IID_IRTCParticipantStateChangeEvent. [hr= %x]", hr);
      return hr;
   }

   hr = pPSCE->get_Participant(&pPart);
   hr = pPSCE->get_State(&enState);

   if (pPart)
   {
      hr = pPart->get_Session(&pSession);
      pPart->Release();
   }
   pPSCE->Release();

   if (enState == RTCPS_CONNECTED || enState == RTCPS_DISCONNECTED)
   {
      // Find the appropriate subscription list and call back to it
      for (itSubMap = subscriptionMap.begin(); itSubMap !=
subscriptionMap.end(); itSubMap++)
      {
         CSubscriptionList *subList = (CSubscriptionList*)
itSubMap->second;
         if (subList->equals(pSession))
         {
            break;
         }
      }

      if (itSubMap != subscriptionMap.end())
      {
         // Send the callback
         CSubscriptionList *subList = (CSubscriptionList*) 
itSubMap->second;
         subList->SendMessageCallBack();
      }

      // TODO: Add checks for any custom structures here
      
   }

   return S_OK;
}

This portion of SubscriptionList.CPP: sends a notification message within the distribution list:

//////////////////////////////////////////////////////////////////////
// Callback from Event Sink when a participantStateChange event occurs for
// the session associated with this Distribution List
//////////////////////////////////////////////////////////////////////

HRESULT CSubscriptionList::SendMessageCallBack()
{
   FX_ENTRY("CSubscriptionList::SendMessageCallBack");
   
   m_nPendingAdditions--;

   FXLog("Global pending == %d\n", m_nPendingAdditions);

   if (m_nPendingAdditions == 0)
   {
      return NotifyList();
   }

   return S_FALSE;
}


//////////////////////////////////////////////////////////////////////
// Sends the Notification Message once all parties are connected to the
// MIM for this Distribution List
//////////////////////////////////////////////////////////////////////

HRESULT CSubscriptionList::NotifyList()
{
   // Send the message on the resident MIM session

   FX_ENTRY("CSubscriptionList::NotifyList");

   HRESULT hr = S_OK;
   
   if (m_pSession == NULL)
      return E_POINTER;

   hr = m_pSession->SendMessage(L"text/plain", m_bstrNotificationMessage,
0);
   if (FAILED(hr))
   {
          FXLog("Send Message failed! [hr=0x%8x]\n", hr);
   }

   return hr;

}

Chat Room Implementation

As previously mentioned, a chat room is a mechanism that allows users to join an existing conversation between multiple users on a certain topic. Within the example application, the code behind chat rooms is contained within PublicSession.CPP and the CPublicSession object. The CPublicSession object is the manager of the multiple chat rooms that can exist. For each chat room that may be ongoing, an instance of the structure PubSessionEntry is created and managed by the CPublicSession object.

This CPublicSession object is accessed based on the instant message command handling previously described, as well as commands being entered by the user. The following section describes the code that is executed for each of the most often used commands:

/CreateChat [name]

When a user issues a command to create a chat room, the EventSink.CPP code calls the ProcessCreateChatCommand function which calls the CPublicSession::CreateSession. This function is responsible for creating the PubSessionEntry object that corresponds to the chat room. At this time, the chat room session is not created—just the information regarding the chat room is created. The room and communications for that room do not exist until a user attempts to join it with /JoinChat, which is described later.

The code to create the chat room information is as follows:

//////////////////////////////////////////////////////////////////////
// Creates a new Public Session (Chatroom) of name bstrSessionName
//////////////////////////////////////////////////////////////////////
HRESULT CPublicSession::CreateSession(BSTR bstrSessionName)
{
   if(!bstrSessionName)
      return E_POINTER;

   PubSessionEntry* pEntry = NULL;
   
   // Make sure it doesn't already exist

   PubSessionEntry *temp = m_pPubSessionListHead;
   PubSessionEntry *prev = NULL;

   while (temp != NULL)
   {
      if (temp->equals(bstrSessionName))
      {
         break;
      }

      prev = temp;
      temp = temp->next;
   }

   if (temp != NULL)
   {
      // Already exists, return failure
      return E_FAIL;
   }
   
   // Now create and add it

   pEntry = new PubSessionEntry();
   if (!pEntry)
   {
      return E_OUTOFMEMORY;
   }

   pEntry->pRTCSession = NULL;
   pEntry->bstrChatRoomName = ::SysAllocString(bstrSessionName);
   pEntry->next = NULL;
   
   if (!(pEntry->bstrChatRoomName))
   {
      return E_OUTOFMEMORY;
   }

   if (m_pPubSessionListHead == NULL)
   {
      m_pPubSessionListHead = pEntry;
   }
   else
   {
      prev->next = pEntry;
   }

   m_nSessionCount++;

   return S_OK;

}

/JoinChat [name]

When a user issues a command to join a chat room, the EventSink.CPP code calls the ProcessJoinChatCommand which calls CPublicSession::AddParticipant for the user. This function is responsible for determining if a chat room session already exists between other users. This is determined by finding the relevant PubSessionEntry for the chat room and checking whether an RTC session has already been created for that chat room. If one has not been created, then a multiparty instant messaging RTC session is created to allow for this and future users to communicate with each other.

Once the existing, or newly created, RTC session is found, the user requesting to join the chat room is added as a participant into that ongoing RTC session. This step ensures that the ongoing multiparty instant messaging session exists to provide the communication channel between all the users of the chat room.

The following sample from PublicSession.CPP: shows how to join a chat room:

//////////////////////////////////////////////////////////////////////
// Adds a Participant of URI bstrParticipantURI  to the public session
// (Chatroom) bstrSessionName
//////////////////////////////////////////////////////////////////////
HRESULT CPublicSession::AddParticipant(BSTR bstrSessionName, BSTR bstrParticipantURI)
{
   FX_ENTRY("CPublicSession::AddParticipant (bstr, bstr)");
   RTC_SESSION_STATE enState;
   IRTCParticipant *pPart = NULL;
   BOOL fNewSession = FALSE;

   if (!bstrSessionName || !bstrParticipantURI)
      return E_INVALIDARG;
   
   HRESULT hr = S_OK;
   PubSessionEntry *pEntry = NULL;
   
   // Find the room first

   PubSessionEntry *temp = m_pPubSessionListHead;

   while (temp != NULL)
   {
      if (temp->equals(bstrSessionName))
      {
         //Found it, break to continue on
         pEntry = temp;
         break;
      }
      temp = temp->next;
   }
   
   if (pEntry == NULL)
   {
      // Couldn't find it... return E_FAIL.
      hr = E_FAIL;
      goto singlePointOfExit;
   }
   
   // Verify that the session is good; else destroy and recreate

   if (pEntry->pRTCSession != NULL)
   {
      pEntry->pRTCSession->get_State(&enState);
      if (enState != RTCSS_CONNECTED)
      {
         pEntry->pRTCSession->Release();
         pEntry->pRTCSession = NULL;
      }
   }
      
   // If no session exists, create one:
   
   if (pEntry->pRTCSession == NULL)
   {
      // Create the Multiparty IM Session
   
      hr = m_pClient->CreateSession(RTCST_MULTIPARTY_IM, NULL, NULL, 0,
(IRTCSession**) &pEntry->pRTCSession);
      if (FAILED(hr))
      {
         FXLog("Failed to Create session [hr=0x%8x]\n", hr);
         goto singlePointOfExit;
      }

      fNewSession = TRUE;
   }

   // Add the participant

   hr = pEntry->pRTCSession->AddParticipant(bstrParticipantURI, NULL,
&pPart);
   if (FAILED(hr))
   {
      FXLog("Failed to Add Participant %S [hr=0x%8x]\n",
bstrParticipantURI, hr);
   }

   if (fNewSession)
   {
      BSTR bstrWelcomeMessage = NULL;
      int nRemainder = 2000;
      wchar_t wszWelcomeMessage[] = L"You are the first person in the
chatroom!";

      bstrWelcomeMessage = ::SysAllocString(wszWelcomeMessage);
      if (!bstrWelcomeMessage)
      {
         return E_OUTOFMEMORY;
      }
      
      pEntry->pRTCSession->SendMessage(NULL, bstrWelcomeMessage, 0);
         ::SysFreeString(bstrWelcomeMessage);
   }
   else
   {
      BSTR bstrWelcomeMessage = NULL;
      wchar_t wszWelcomeMessage[MAX_STRING];
      _snwprintf(wszWelcomeMessage, MAX_STRING, L"%s has joined the
chatroom.", bstrParticipantURI);
      wszWelcomeMessage[MAX_STRING-1] = 0;

      bstrWelcomeMessage = ::SysAllocString(wszWelcomeMessage);
      if (!bstrWelcomeMessage)
      {
         return E_OUTOFMEMORY;
      }
      
      pEntry->pRTCSession->SendMessage(NULL, bstrWelcomeMessage, 0);
         ::SysFreeString(bstrWelcomeMessage);
   }
      

   singlePointOfExit:

   if (pPart) pPart->Release();

   // Don't release the session or messageSend because they are in the map
   return hr;

}

The request from the user to the bot to join a chat room occurs in one RTC session. However, the chat room is a different RTC session. Therefore, the user's client issues the command to join the chat room, and then receives a new incoming session that is the actual chat room with all the participants.

Deploying a Bot with Live Communications Server

Once a bot is developed using the RTC Client API, it still needs to be deployed so it can communicate with a SIP server to provide the rendezvous and routing functionality. The following section details the steps required to deploy a bot with Microsoft Office Live Communications Server 2003.

Bot Account

The first step in deploying the bot with Live Communications Server is to create an account that is enabled for the bot. This account is used for the following two factors:

  1. It allows the bot application to be verified and authenticated by Live Communications Server. Without this, any user can attempt to connect to a computer running Live Communications Server as the bot and receive instant messages from other users. In some cases, this means the user can receive confidential information.
  2. It allows Live Communications Server to be aware of the computer running the bot, and to route all requests intended for the bot to the relevant computer.

An account can be created using the MMC console, just as it can for any other user within Live Communications Server. For more information about creating an account within Live Communications Server please see the Live Communications Server 2003 Deployment Guide at the Microsoft Download Center.

Improving the User Experience

Depending upon the user, he or she might want to add the bot as a member of the contact list. This allows users to start instant messaging sessions by double-clicking the contact, rather than having to enter the address every time they want to communicate with the bot.

If the user is using Windows Messenger, by adding the bot to the contact list, the client application is also requesting for the presence information of the bot. For a bot application, its presence state (busy, on the phone, and so forth) is not relevant as it is likely to be always online. Obtaining presence information is not required. However, multiple client applications requesting this presence information adds additional network traffic to the server.

In addition, every time a user adds the bot to the contact list, there is a higher chance of hitting the 150-contact limit within the Live Communication Server. This specific limit is related to the maximum number of contacts that anyone user can have, as well as the maximum number of users that can be monitoring a particular user.

To allow the user to have the bot as a contact, the bot must be added to the contact list in a different fashion. In effect, the bot must be added as a contact where presence information is not required. This can be achieved in two ways:

  1. Develop an application that uses the RTC Client API that the user can run. The application takes the user's account information, logs on to the server, and adds the bot as a contact without requiring presence information. Because the server supports a roaming contact list, this contact is available in the user's client application. For more information about how to add a contact without requiring presence information, see the RTC Client API details for IRTCClientPresence2::AddBuddyEx in the Microsoft MSDN Library.
  2. Develop an application that uses the Live Communications Server Management API and run by the server administrator. Ensure that users cannot subscribe to this contact. The following is a code sample highlighting how to add a contact through the Management API. For more information about how to add a contact using the Management API, see the Live Communications Server Reference Guide available at the Microsoft Download Center.
Private Function CreateOfflineContact(UserInstanceID, SIPURI, GroupID, Name, ExternalURL)

      Dim ReturnValue
      ReturnValue = True

      Dim DefaultContactInstance       
      Dim NewContactInstance

      Wscript.Echo "Adding Contact: " & SIPURI & " " & ExternalURL
      Set DefaultContactInstance = GetObject
   ("WinMgmts:MSFT_SIPESUserContactData")

      ' Do error checking here

      Set NewContactInstance = DefaultContactInstance.SpawnInstance_

      '//populating data
      NewContactInstance.UserInstanceID = UserInstanceID
      NewContactInstance.SIPURI = SIPURI
      NewContactInstance.GroupID = GroupID
      NewContactInstance.Name = Name
      NewContactInstance.ExternalURL = ExternalURL
      NewContactInstance.Subscribed = False

      Err.Clear()         
      NewContactInstance.Put_ 0   'create only

      ' Do error checking here

      CreateOfflineContact = ReturnValue

   End Function

Conclusion

Building a bot that utilizes the RTC Client API, and the SIP infrastructure deployed within an organization enables some powerful integration scenarios. Utilizing the rendezvous capabilities of the SIP infrastructure, as well as the standard instant messaging user interface for the user, provides a common framework and deployment mechanism for these bots.

With the example application described in this white paper, and the sample application to build a bot, it is possible to add additional innovate functionality to extend the capabilities of a bot to fit any business need.

Related Links

See the following resources for further information:

© 2009 Microsoft Corporation. All rights reserved. Terms of Use | Trademarks | Privacy Statement
Page view tracker