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.
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.
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.
<code class="bash">
%ProgramFiles%\Microsoft Lync Server 2013\Deployment\Bootstrapper.exe
</code>
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
<code class="bash">
$myurl = New-CsWebOrigin -Url "{https://mysite}"
Set-CsWebServiceConfiguration -Identity "{myidentity}" -CrossDomainAuthorizationList @{Add=$myurl}
</code>
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.
- Initialize the SDK
- Authenticate with user credentials
- Get the user’s list of contacts
- Subscribe to presence changes for each contact
- Logout
- 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.
<code class="html">
<script src="https://swx.cdn.skype.com/shared/v/1.1.23.0/SkypeBootstrap.min.js"></script>
</code>
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.
<code class="javascript">
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);
});
</code>
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.
<code class="javascript">
client.signInManager.signIn({
username: ‘matthew@contoso.com’,
password: ‘p@ssw0rd!’
})
</code>
In this project, we really don’t want to hard-code credentials, so instead lets collect them from forms on the web page.
<code class="javascript">
client.signInManager.signIn({
username: $('#username').text(),
password: $('#password').text()
})
</code>
We should handle authentication errors gracefully, so lets add a handler to let us know when authentication is successful, or when it fails.
<code class="javascript">
application.signInManager.signIn({
username: $('#username').text(),
password: $('#password').text()
}).then(
function () {
log('Signed in as'+ application.personsAndGroupsManager.mePerson.displayName());
},
function (error) {
log(error || 'Cannot sign in');
})
});
</code>
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.
<code class="javascript">
application.signInManager.state.changed(function (state) {
log('Application has changed its state to: ' + state);
});
</code>
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.
<code class="javascript">
client.personsAndGroupsManager.all.persons.get().then(function (persons) {
...
})
</code>
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.
<code class="javascript">
persons.forEach(function (person) {
person.displayName.get().then(function (name) {
var tag = $('<p>').text(name);
log(‘Person found: ‘ + tag);
})
});
</code>
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:
<code class="javascript">
person.status.subscribe();
</code>
Of course, we need to catch the change event, and actually update the card appropriately.
<code class="javascript">
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);
}
});
</code>
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 )
<code class="javascript">
$('#signout').click(function () {
application.signInManager.signOut()
.then(
function () {
log('Signed out');
},
function (error) {
log(error || 'Cannot sign in');
});
});
</code>
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.
<code class="javascript">
function imgError(image) {
image.onerror = "";
image.src = "noimage.png";
return true;
}
</code>
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.
<code class="javascript">
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;
}
</code>
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
<code class="html">
<!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.10.1.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>
</code>
<code class="javascript">
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 = '';
var bs_footer = '';
$(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)...');
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) {
log('Found Collection');
$('#dashboardtiles').append(bs_header);
persons.forEach(function (person) {
person.displayName.get().then(function (name) {
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();
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) {
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 (presence_state != '') {
person.avatarUrl.get().then(function (url) {
$('#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) {
$('#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) {
log(error || 'Something went wrong.');
});
log('Finished');
})
$('#signin').click(function () {
$('#signin').hide();
var pSearch;
log('Signing in...');
client.signInManager.signIn({
username: $('#address').text(),
password: $('#password').text()
}).then(function () {
log('Logged In Succesfully');
$('#everyone').show();
$('#loginbox').hide();
}).then(null, function (error) {
log('Oops, Something went wrong: '+ error);
$('#loginbox').show()
});
});
});
</code>
<code class="css">
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;
}
</code>
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!