Creating a Presence Dashboard using the Skype Web SDK


Saturday, 16 May 2015

Share with: 
 

 

What is a Presence Dashboard

A Presence Dashboard is a web-based (or application-based) tool that provides a single view of all your Lync or Skype for Business contacts and their current status.

A number of commercial dashboards are available in the market today, however you can easily build your own (or download mine) for free using the recently released Skype Web SDK.

I've updated this dashboard as a new version with additional features, outlined in Skype for Business Presence Dashboard Revisited.

 

Overview

The Skype for Business (formerly Lync 2013) client displays presence effectively, next to each person in the groups or status list. This information, however, can’t be easily exported or displayed on a web site, as it is part of the client application.


 

Technology Required

To build a presence dashboard using the Skype Web SDK, all you need is the SDK, a web server to host the framework, and of course your Skype for Business account details.

We’ll be using the web framework bootstrap, in order to provide a clean and responsive layout, as well as jQuery, to simplify our JavaScript.

This project assumes you have an understanding of JavaScript and jQuery. If not, the internet is full of great tutorials, or check out Puralsight.

This project doesn’t require any server side components, apart from a Lync 2013 or Skype for Business environment that supports the UCWA framework. If don’t already have UCWA installed on your environment, pop over here to learn how to install it.

 

The Presence Dashboard

The objective of this project is to construct a flexible ‘dashboard’ that displays your contacts’ presence status – typically Online, Away, Busy, Offline and Do-Not-Disturb.

In the image below, you can see what we’ve colour coded a series of tiles to closely represent the colours used in the Skype for Business client.

Example dashboard

Photographs (or Avatars, as they are inconsistently referred to in documentation) are loaded for each card, which helps put a face to each name, so to speak. Where a photograph isn’t found, a generic image is displayed instead.

Once the dashboard is running, it will update automatically whenever a contact changes their presence, using the subscription model described below.

 

Prerequisites - Install UCWA

The Skype Web SDK requires that the UCWA framework is installed and enabled on your Front-End, Edge and Director servers. If you're not already using UCWA, then you'll need to perform the following steps.

Pre-Step 1 – Upgrade Your Servers

Ensure that all your servers are running Lync Server 2013 CU1 or higher, or better still, Skype for Business 2015

Pre-Step 2 – "Boostrap" Your Servers

The Bootstrapper application, built into Lync 2013 and above, needs to be executed on each of the servers above. This can be performed using the command shown below.


    %ProgramFiles%\Microsoft Lync Server 2013\Deployment\Bootstrapper.exe

Pre-Step 3 – Configure Trusted Connections

You now need to tell your Lync or Skype for Business servers which domains to trust connections from. To do this, you simply use the

Set-CsWebServiceConfiguration
commandlet from a Lync or Skype Management Shell that has been started with Administrative privileges


    $myurl = New-CsWebOrigin -Url "{https://mysite}"
    Set-CsWebServiceConfiguration -Identity "{myidentity}" -CrossDomainAuthorizationList @{Add=$myurl}

Don't forget to replace 'mysite' with the fully qualified domain the web site on which you'll be using the Skype Web SDK. You should also replace 'myidentity' with something more relevant to your environment.

These steps must be performed on every Front-End, Edge and Director server you have.

Instructions for this can also be found in my Pluralsight course!

 

The Code

The application performs a few basic steps, which are outlined below.

  1. Initialize the SDK
  2. Authenticate with user credentials
  3. Get the user’s list of contacts
  4. Subscribe to presence changes for each contact
  5. Logout
  6. Miscellaneous Functions

The full code of for this application is listed below.

If you’ve already built an application using the Skype Web SDK, you can probably skip steps 1 & 2.

Step 1 – Initializing the SDK

The first step is to actually initialize the SDK. This requires the inclusion of the actual SDK in your HTML file, and this is made very easy as Microsoft conveniently provide this on their CDN.


<script src="https://swx.cdn.skype.com/shared/v/1.1.23.0/SkypeBootstrap.min.js"></script>

The version of the script may change from time to time, so it’s always best to check http://developer.skype.com regularly.

The SDK is exposed to JavaScript with the "Skype." prefix, which is case-sensitive.

Within your JavaScript file, we now need to initialize the SDK, which we can achieve by calling the SDK’s initialize method.


var Application
var client;
Skype.initialize({
    apiKey: 'SWX-BUILD-SDK',
        }, function (api) {
            Application = api.application;
            client = new Application();
        }, function (err) {
            log('An error occurred initializing the application: ' + err);
});
                

In this example, we’re creating an application object, called (obviously) Application. The Application object is created by calling the application constructor and is the entry point to the SDK, and we will create a new instance of this called client.

Step 2 - Authenticate with user credentials

Once the application has been initialized, the next step is to authenticate against the server using your sign-in credentials.


// start signing in
client.signInManager.signIn({
    username: ‘matthew@contoso.com’,
    password: ‘p@ssw0rd!’
})
            


In this project, we really don’t want to hard-code credentials, so instead lets collect them from forms on the web page.


// start signing in
client.signInManager.signIn({
    username: $('#username').text(),
    password: $('#password').text()
})
            


We should handle authentication errors gracefully, so lets add a handler to let us know when authentication is successful, or when it fails.


// start signing in
application.signInManager.signIn({
    username: $('#username').text(),
    password: $('#password').text()
}).then(
    //onSuccess callback
    function () {
        // when the sign in operation succeeds display the user name
        log('Signed in as'+ application.personsAndGroupsManager.mePerson.displayName());
    },
    //onFailure callback
    function (error) {
        // if something goes wrong in either of the steps above,
        // display the error message
        log(error || 'Cannot sign in');
    })
});
            

To help know whether the Application remains signed in, or indeed it’s state at any time, it’s useful to set up a subscription to the SignInManager.state.change method.


// whenever state changes, display its value
application.signInManager.state.changed(function (state) {
    log('Application has changed its state to: ' + state);
});
            

Step 3 - Get the user’s list of contacts

Before we can display anyone’s presence status, we need to collection of contacts (‘persons’) and iterate through them, retrieving key information to display.

This is achieved by the rather verbose call to the persons.get() method.


client.personsAndGroupsManager.all.persons.get().then(function (persons) {
        ...
})
            

A person object in Skype Web SDK represents a single person, and contains all the information the user publishes from presence information and a photograph, to telephone numbers, job title and current location.

When then iterate through the persons.get() object to discover each contact.


persons.forEach(function (person) {
    person.displayName.get().then(function (name) {
        var tag = $('<p>').text(name);
        log(‘Person found: ‘ + tag);
    })
});
            

Step 4 - Subscribe to presence changes for each contact

As we iterate through the list of contacts in Step 3, we need to attach a subscription request to each – triggering an update to the card when the contact’s subscription status changes – for example from Online to Away.

This is as simple as:


person.status.subscribe();
            


Of course, we need to catch the change event, and actually update the card appropriately.


person.status.changed(function (status) {
    log(name + ' availability status is changed to ' + status);
    var d = new Date();
    var curr_hour = d.getHours();
    var curr_min = d.getMinutes();
    var curr_sec = d.getSeconds();
    var new_presence_state = '';
    if (status == 'Online') {
        new_presence_state = 'alert alert-success';
    }
    else if (status == 'Away') {
        new_presence_state = 'alert alert-warning';
    }
    else if (status == 'Busy') {
        new_presence_state = 'alert alert-danger';
    }
    else {
    if ($('#showoffline').is(":checked")) {
        new_presence_state = 'alert alert-info';
    }
    }
    if (new_presence_state != '') {
        log(name + ‘ has a new presence status of ‘ + new_presence_state);
    }
});
            

Step 5 - Logout

When you want to close the dashboard, it’s recommended that you log out using the SDK’s functions, as opposed to just closing the browser or navigating away. This ensures that the session to Lync 2013 or Skype for Business is closed correctly.

(Code from https://msdn.microsoft.com/EN-US/library/dn962162%28v=office.16%29.aspx )


// when the user clicks on the "Sign Out" button
$('#signout').click(function () {
    // start signing out
    application.signInManager.signOut()
    .then(
    // onSuccess callback
    function () {
        // and report the success
        log('Signed out');
    },
    // onFailure callback
    function (error) {
        // or a failure
        log(error || 'Cannot sign in');
    });
});
         

Step 6 – Miscellaneous Functions

Ok, not really a step, but within my code examples I’ve used a few extra functions to help with layout and image processing.

The first is imgError() – this function simply returns a reference to a generic ‘little grey person’ PNG file where a contact doesn’t have a valid photograph.


function imgError(image) {
    image.onerror = "";
    image.src = "noimage.png";
    return true;
}

The noimage.png can be found here:   

Throughout the project, I’m reporting status and updates via a log() function, instead of using the alert() function. Aside from being annoying when it pops up a message, alert() is not overly flexible. Instead, my log() function simply prepends the text message to a DIV, with the current time stamp – very useful for debugging and seeing activities in real time.


function log(texttolog) {
    var d = new Date();
    var time = padLeft(d.getHours(), 2) + ":" + padLeft(d.getMinutes(), 2) + ":" + padLeft(d.getSeconds(), 2) + ":" +
    padLeft(d.getMilliseconds(), 3);
    $('#logging_box').append(time + ": " + texttolog + "<br>");
}
function padLeft(nr, n, str) {
    return Array(n - String(nr).length + 1).join(str || '0') + nr;
}
            


There’s also a simple padLeft() function that pads time references with 0’s where they are less than 2 digits long, to make them nice and consistent.

 

The Full Code


<!doctype html>
<html>
<head>
    <title>Skype Web SDK - Matthew's Presence Dashboard</title>
    <!-- SkypeWeb library requires IE compatible mode turned off -->
    <meta http-equiv="X-UA-Compatible" content="IE=edge">

    <link rel="shortcut icon" href="//www.microsoft.com/favicon.ico?v2">
    <link href="index.css" rel="stylesheet">

    <!-- the jQuery library written by John Resig (MIT license) -->
    <script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.11.3.min.js"></script>

    <!-- SkypeWebSDK Bootstrap Libray -->
    <script src="https://swx.cdn.skype.com/shared/v/1.1.23.0/SkypeBootstrap.min.js"></script>

    <!-- Load Bootstrap -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>

    <!-- index.js is just a sample that demonstrates how to use lync.js -->
    <script src="index.js"></script>


</head>
<body>
    <div class="signinframe">

        <div id="loginbox">
            <div>Login</div>
            <div id="address" contenteditable="true" class="input form-control"></div>
            <div>Password</div>
            <input type="password" id="password" name="password" class="input form-control" />

            <!--
            <div>Search Query</div>
            <div id="query" contenteditable="true" class="input">ba</div>
            -->
            <div id="signin" class="button">Sign-in</div>
            <!--
            <div id="searchagain" class="button">Search</div>
            -->
        </div>
        <div id="everyone" class="button">Create Dashboard</div>
        <div class="checkbox">
            <label>
                <input type="checkbox" id="showoffline"> Show offline contacts
            </label>
        </div>

        <div id="status"></div>
        <div id="results"></div>
    </div>

    <div class="container">
        <div class="row">
            <div class="col-md-2"><p class="alert">Legend:</p></div>
            <div class="col-md-2"><p class="alert alert-success">Online</p></div>
            <div class="col-md-2"><p class="alert alert-warning">Away</p></div>
            <div class="col-md-2"><p class="alert alert-danger">Busy</p></div>
            <div class="col-md-2"><p class="alert alert-info">Offline</p></div>
        </div>


        <div class="row" id="stuff">
        </div>

        <div class="row">
            <div class="col-md-12" id="updatelabel"></div>
        </div>
    </div>

    <div class="container">
        <div id="logging_box" contenteditable="false" class="code"><b>Event Logs<br /></b></div>
    </div>

</body>
</html>


/* 
   Generic Functions
*/
function log(texttolog) {
    var d = new Date();
    var time = padLeft(d.getHours(), 2) + ":" + padLeft(d.getMinutes(), 2) + ":" + padLeft(d.getSeconds(), 2) + ":" + padLeft(d.getMilliseconds(), 3);
    $('#status').text(texttolog);
    $('#logging_box').append(time + ": " + texttolog + "<br>");
}
function padLeft(nr, n, str) {
    return Array(n - String(nr).length + 1).join(str || '0') + nr;
}
function imgError(image) {
    image.onerror = "";
    image.src = "noimage.png";
    return true;
}

var bs_header = ''; //'<div class=\"container\"><div class=\"row\">';
var bs_footer = ''; //'</div></div>';

/* 
   Presence Dashboard Example for the Skype Web SDK 
*/
$(function () {
    'use strict';

    log("App Loaded");

    var Application
    var client;
    Skype.initialize({
        apiKey: 'SWX-BUILD-SDK',
    }, function (api) {
        Application = api.application;
        client = new Application();
    }, function (err) {
        log('some error occurred: ' + err);
    });

    log("Client Created");

    $('#everyone').hide();
    $('#searchagain').hide();

    $('#searchagain').click(function () {
        var pSearch;
        log('Search Again Clicked');
        pSearch = client.personsAndGroupsManager.createPersonSearchQuery();
        log('Searching for ' + $('#query').text());
        pSearch.text.set($('#query').text());
        pSearch.limit.set(100);
        pSearch.getMore().then(function (results) {
            log('Processing search results (2)...');
            //console.log('Search person results', results);

            // display all found contacts
            results.forEach(function (r) {
                var tag = $('<p>').text(r.result.displayName());
                $('#results').append(tag);
            });
            log('Finished');

        })
    })

    $('#everyone').click(function () {
        log('Everyone Clicked');

        var thestatus = '';
        var destination = '';

        client.personsAndGroupsManager.all.persons.get().then(function (persons) {
            // `persons` is an array, so we can use Array::forEach here
            log('Found Collection');
            $('#dashboardtiles').append(bs_header);

            persons.forEach(function (person) {
                // the `name` may not be loaded at the moment
                person.displayName.get().then(function (name) {

                    // now subscribe to the status change of everyone - so that we can see who goes offline/away/busy/online etc.
                    person.status.changed(function (status) {
                        $("#updatelabel").val(name + ' is now ' + status);

                        var d = new Date();
                        var curr_hour = d.getHours();
                        var curr_min = d.getMinutes();
                        var curr_sec = d.getSeconds();
                       
                        var new_presence_state = '';
                        if (status == 'Online') {
                            new_presence_state = 'alert alert-success';
                        }
                        else if (status == 'Away') {
                            new_presence_state = 'alert alert-warning';
                        }
                        else if (status == 'Busy') {
                            new_presence_state = 'alert alert-danger';
                        }
                        else {
                            if ($('#showoffline').is(":checked")) {
                                new_presence_state = 'alert alert-info';
                            }
                        }
                        if (new_presence_state != '') {
                            var name_id = name.replace(/[^a-z0-9]/gi, '');
                            $('#status' + name_id).attr('class', new_presence_state);
                        }
                    });
                    person.status.subscribe();
                    
                    // if the name is their email address, drop the domain component so that it's a little more readable
                    var name_shortened = name.split("@")[0];
                    var name_id = name.replace(/[^a-z0-9]/gi, '');

                    var tag = $('<p>').text(name);

                    person.status.get().then(function (status) {

                        //select a bootstrap helper style that reasonably approximates the Skype presence colours.
                        var presence_state = '';
                        if (status == 'Online') {
                            presence_state = 'alert alert-success';
                            destination = 'contact_online';
                        }
                        else if (status == 'Away') {
                            presence_state = 'alert alert-warning';
                            destination = 'contact_away';
                        }
                        else if (status == 'Busy') {
                            presence_state = 'alert alert-danger';
                            destination = 'contact_busy';
                        }
                        else {
                            if ($('#showoffline').is(":checked")) {
                                presence_state = 'alert alert-info';
                                destination = 'contact_offline';
                            }
                        }
                        // if a presence has been determined, display the user.
                        if (presence_state != '') {
                            //$('#dashboardtiles').append("<div class=\"col-md-3 \" id=\"" + name + "\"><p class=\"" + presence_state + "\">" + name_shortened + " (" + status + ")</p></div>");

                            //now get their Photo/Avatar URL
                            person.avatarUrl.get().then(function (url) {
                                //successfully received an avatar/photo
                                $('#dashboardtiles').append("<div class=\"col-sm-3 \" id=\"" + name + "\"><p id=\"status" + name_id + "\" class=\"" + presence_state + "\"><img hspace=5 src=\"" + url + "\" width=32  onError=\"this.onerror=null;this.src='noimage.png';\" />" + name_shortened + "</p></div>");
                            }).then(null, function (error) {
                                // display the user with the generic user image
                                $('#dashboardtiles').append("<div class=\"col-sm-3 \" id=\"" + name + "\"><p id=\"status" + name_id + "\" class=\"" + presence_state + "\">" + name_shortened + "</p></div>");
                            });
                        }
                    });
                });
            });

            $('#dashboardtiles').append(bs_footer);


        }).then(null, function (error) {
            // if either of the operations above fails, tell the user about the problem
            //console.error(error);
            log(error || 'Something went wrong.');
        });

        log('Finished');
    })

    // when the user clicks the "Sign In" button
    $('#signin').click(function () {
        $('#signin').hide();

        // create an instance of Client

        var pSearch;

        log('Signing in...');

        // and invoke its asynchronous "signIn" method
        client.signInManager.signIn({
            username: $('#address').text(),
            password: $('#password').text()
        }).then(function () {
            log('Logged In Succesfully');
            $('#everyone').show();
            $('#loginbox').hide();
            //$('#searchagain').show();
        }).then(null, function (error) {
            // if either of the operations above fails, tell the user about the problem
            //console.error(error);
            log('Oops, Something went wrong: '+ error);
            $('#loginbox').show()
        });
    });
});


body {
    font: 11pt calibri;
}

.input {
    border: 1pt solid gray;
    padding: 2pt;
    overflow: hidden;
    white-space: nowrap;
}

.button {
    border: 1pt solid gray;
    cursor: pointer;
    padding: 2pt 5pt;
    display: inline-block;
}

.button:hover {
    background: lightgray;
}

.signinframe {
    padding: 10pt 30%;
}

.signinframe > div {
    margin-bottom: 3pt;
}

.signinframe > .button {
    margin-top: 8pt;
}

 

Very Important Considerations

The code examples here require the framework to be logged in as you (or using an account that has your contacts/groups configured as well) – so be careful with your credentials.

We recommend running this code on a site protected by SSL, and under no circumstances hard-coding your credentials into the code. If you do, it’s at your own peril!

 

Further Exploration

Additional features could be easily added to the dashboard, including:

  • Custom presence statuses
  • Updating the card photograph when it’s changed by a contact
  • Including the contact’s location or note

Tags

Skype, Skype Web SDK, UCJA, JLync, UCWA, CodeProject, Dashboard
The Skype Web SDK is a powerful new framework built upon UCWA that allows you to rapidly build Skype-enabled applications and functionality. In this tutorial, learn how to build a Presence Dashboard to display your contacts and their availability status in real time.
 
 

Popular Articles

Kilimanjaro 2015
Exploring Lync and IoT
Exchange 2013 in 60 minutes
Monitoring Lync with MRTG
Lync UCWA Tutorial - Introduction
Tutorial Parts 1 | 2 | 3 | 4 | 5

Recent Articles

Australian Postal Codes
Skype Web SDK
Using the Skype Web SDK from any language or framework
Extracting Email Addresses from Outlook Mailboxes using C#
Building a Skype for Business Auto Responder using the Skype Web SDK
Exporting Lync or Skype for Business Contacts with the Skype Web SDK

Favourite Links

Kutamo
Telco Together Foundation
Cloud on Kilimanjaro