Introduction
Many companies I work for are using Lync for communication inside and outside the company, unlike most IM tools Lync doesn’t allow offline messaging. Which means that when you try to send a message to a contact that is offline you’ll receive the following message:
We couldn't send this message because User is unavailable or offline.
So I decided to provide a workaround (no I’m not adding offline messaging to Lync) that allows me to send an SMS message to anyone directly from Lync (also pulling user name Mobile Number).
* Sending SMS requires a server that supports that *
<td<img src="/KB/cs/993654/1.png">
Context Menu | Conversation Window Extension (CWE) |
| |
Using the code
Before we start make sure you have the following in order to build the project:
Supported operating system software:
- Microsoft Windows Server 2008 R2
- Microsoft Windows 7 (64-bit)
- Microsoft Windows 7 (32-bit)
Required software:
Project structure:
- Silverlight – Lync 2013 CWE application must be Silverlight
- WPF – Will be lunch from Context menu and from Desktop.
- Shared – Classes with Lync API that need to be compiled in both Silverlight and Wpf Apps.
- Common - Portable Class library with all Send SMS assets.
Because CWE and Context menu apps are different (Silverlight and WPF) I had to rewrite the UI twice but I’ve use the same code base using Shared and Portable library.
Helpers
HttpUtility.cs – Native encoding Url from portable library.
public static class HttpUtility
{
public static string UrlEncode(string str, Encoding e)
{
if (str == null)
return null;
byte[] bytes = UrlEncodeToBytes(str, e);
return Encoding.UTF8.GetString(bytes, 0, bytes.Length);
}
public static string UrlEncode(string str)
{
if (str == null)
return null;
return UrlEncode(str, Encoding.UTF8);
}
private static byte[] UrlEncodeToBytes(string str, Encoding e)
{
if (str == null)
return null;
byte[] bytes = e.GetBytes(str);
return UrlEncodeBytesToBytesInternal(bytes, 0, bytes.Length, false);
}
private static byte[] UrlEncodeBytesToBytesInternal(byte[] bytes, int offset, int count, bool alwaysCreateReturnValue)
{
int cSpaces = 0;
int cUnsafe = 0;
for (int i = 0; i < count; i++)
{
char ch = (char)bytes[offset + i];
if (ch == ' ')
cSpaces++;
else if (!IsSafe(ch))
cUnsafe++;
}
if (!alwaysCreateReturnValue && cSpaces == 0 && cUnsafe == 0)
return bytes;
byte[] expandedBytes = new byte[count + cUnsafe * 2];
int pos = 0;
for (int i = 0; i < count; i++)
{
byte b = bytes[offset + i];
char ch = (char)b;
if (IsSafe(ch))
{
expandedBytes[pos++] = b;
}
else if (ch == ' ')
{
expandedBytes[pos++] = (byte)'+';
}
else
{
expandedBytes[pos++] = (byte)'%';
expandedBytes[pos++] = (byte)IntToHex((b >> 4) & 0xf);
expandedBytes[pos++] = (byte)IntToHex(b & 0x0f);
}
}
return expandedBytes;
}
private static char IntToHex(int n)
{
Debug.Assert(n < 0x10);
if (n <= 9)
return (char)(n + (int)'0');
else
return (char)(n - 10 + (int)'a');
}
private static bool IsSafe(char ch)
{
if (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch >= '0' && ch <= '9')
return true;
switch (ch)
{
case '-':
case '_':
case '.':
case '!':
case '*':
case '\'':
case '(':
case ')':
return true;
}
return false;
}
}
Settings.cs - Main settings file that contains the SMS server url, request method and more.
If your SMS server is based on rest or SOAP you can define it in the settings file, keep the arguments in the URL as it is, for example:
http://www.freesmsservice.com/sendSMS?Phones={0}&Message={1}
The app will inject the message and phone numbers into the URL.
public class Settings
{
public string SendButtonText = "Send";
public string ClearButtonText = "Clear";
public string NoPhoneFoundMessage = "Contact doesn't have mobile number defined.";
public string NoPhonesEnteredMessage = "Please enter at least on mobile number.";
public string NoMessageEnteredMessage = "Please enter SMS message and try again.";
public string InvalidPhoneNumber = "Invalid Phone Number";
public string Loading = "Loading...";
public string SuccessMessage = "SMS has been sent successfully!";
public string Busy = "Sending in progress...";
public bool RTL = false;
public int MaxSmsChars = 70;
public WebRequestType WebRequestType = WebRequestType.Rest;
public string ServiceUrl = "<a href="http:
public string RestUrl = "<a href="http:
public string ServiceMethod;
public string ServicePhonesParamName;
public string ServiceMessageParamName;
public HttpMethod RestMethod { get; set; }
}
If you don’t want to change the code and use the external settings files you can edit the html file located in the installation directory and it will override the default settings. (LyncSMSContextAddinPage.html)
<param name="initParams" value="
SendButtonText=Send,
ClearButtonText=Clear,
NoPhoneFoundMessage=Contact doesn't have mobile number defined.,
NoPhonesEnteredMessage=Please enter at least on mobile number.,
NoMessageEnteredMessage=Please enter SMS message and try again.,
Loading=Loading...,
SuccessMessage=SMS has been sent successfully!,
Busy=Sending in progress...,
InvalidPhoneNumber=Invalid Phone Number,
MaxSmsChars=70,
RTL=False,
WebRequestType=Rest,
ServiceUrl=http://www.demoservice.com/service.svc,
ServicePhonesParamName=phonesList,
ServiceMessageParamName=message,
ServiceMethod=SendSMS,
RestUrl=http://www.demoservice.com/service?Phones={0}&Message={1}" />
<!--
<!--
<!--
<!--
<!--
WPF Application
When you call external application from Lync Context Menu you can pass additional parameters, such as Contact info and more.
[Add custom commands to Lync menus - https://msdn.microsoft.com/EN-US/library/jj945535.aspx]
For example: Path="C:\\ExtApp1.exe /userId=%user-id% /contactId=%contact-id%"
I’m using those parameters to pass the location of the HTML file htmlPath(the location can be changed in the Setup wizard) and the contactId which contains the phone numbers to send the SMS too.
To support Application arguments I’ve modified the OnStartup method in App.xaml.cs file.
protected override void OnStartup(StartupEventArgs e)
{
App.Current.DispatcherUnhandledException += Current_DispatcherUnhandledException;
try
{
string argsParam = @"/contactId:Contacts=";
string argsHtmlParam = @"/htmlPath:";
if (e.Args.Length == 0) return;
foreach (string arg in e.Args)
{
if (arg.StartsWith(argsParam))
{
int startIndex = arg.IndexOf(argsParam, System.StringComparison.Ordinal) + argsParam.Length;
var contacts = arg.Substring(startIndex);
Params.Contacts = contacts;
}
if (arg.StartsWith(argsHtmlParam))
{
int startIndex = arg.IndexOf(argsHtmlParam, System.StringComparison.Ordinal) + argsHtmlParam.Length;
string htmlFile = "";
htmlFile = arg.Substring(startIndex);
Params.HtmlFile = htmlFile;
}
}
}
catch (Exception ex)
{
MessageBox.Show("Reading Startup Arguments Error - " + ex.Message);
}
}
Let’s move to MainWindow.xaml.cs, our core for WPF app, there are two options for initialize the ViewModel, with Html file that contains the parameters or using the Default values.
The first line in the constructor will call – DefineVMModel method to check if there is an HTML file param and if so it will parse it into Dictionary<string, string>.
private void DefineVMModel()
{
if (string.IsNullOrEmpty(Params.HtmlFile) || !File.Exists(Params.HtmlFile))
{
_vm = new MainViewModel();
return;
}
try
{
using (StreamReader sr = new StreamReader(Params.HtmlFile))
{
string fileContent = sr.ReadToEnd();
int startIndex = fileContent.IndexOf(HtmlFileParamsArg, System.StringComparison.Ordinal) +
HtmlFileParamsArg.Length;
int endIndex = fileContent.IndexOf("\"", startIndex, System.StringComparison.Ordinal);
string values = fileContent.Substring(startIndex, (endIndex - startIndex))
.Replace("\r\n", string.Empty);
string[] valuesArray = values.Split(',');
Dictionary<string, string> dictionary = valuesArray.ToDictionary(item => item.Split(new[] { '=' }, 2, StringSplitOptions.RemoveEmptyEntries)[0].Trim(),
item => item.Split(new[] { '=' }, 2, StringSplitOptions.RemoveEmptyEntries)[1].Trim());
_vm = new MainViewModel(dictionary);
}
}
catch (Exception ex)
{
}
}
The second action in the constructor will be getting the contact phone number (assuming Contacts param available), this will require to acquire Lync client (You must add Lync SDK at this point).
(See additional data in comments inline)
try
{
client = LyncClient.GetClient();
while (client.Capabilities == LyncClientCapabilityTypes.Invalid)
{
System.Threading.Thread.Sleep(100);
client = LyncClient.GetClient();
}
client.ClientDisconnected += client_ClientDisconnected;
client.StateChanged += client_StateChanged;
if (string.IsNullOrEmpty(Params.Contacts))
return;
List<Contact> contacts = new List<Contact>();
foreach (string contactSip in Params.Contacts.Split(','))
{
var contact = client.ContactManager.GetContactByUri(contactSip.Replace("<", string.Empty).Replace(">", string.Empty));
contacts.Add(contact);
}
foreach (Contact contact in contacts)
{
List<object> endpoints = (List<object>)contact.GetContactInformation(ContactInformationType.ContactEndpoints);
foreach (ContactEndpoint phone in endpoints.Cast<ContactEndpoint>().Where
(phone => phone.Type == Microsoft.Lync.Model.ContactEndpointType.MobilePhone))
{
_vm.AddContact(phone.DisplayName);
}
}
}
catch (Exception exception)
{
MessageBox.Show(exception.Message, "Error While GetClient", MessageBoxButton.OK, MessageBoxImage.Error);
}
Regarding the UI, it’s pretty simple, the XAML is bind to the ViewModel, I’ll talk about the VM later in the post.
using Lync SDK allows me to add Lync Controls to my UI:
<controls:ContactSearchInputBox x:Name="contactSearchInputBox" VerticalAlignment="Top" Grid.Row="1" MaxResults="15" Margin="0"/>
<controls:ContactSearchResultList
Grid.Row="2" ItemsSource="{Binding ElementName=contactSearchInputBox, Path=Results}"
ResultsState="{Binding SearchState, ElementName=contactSearchInputBox}" SelectionMode="Single" SelectionChanged="ContactSearchResultList_SelectionChanged" Grid.ColumnSpan="2" Margin="0,0,-0.333,0">
</controls:ContactSearchResultList>
Silverlight
The Silverlight app use the same concept as the WPF app except that Silverlight automatically receives the InitParams from the Html file in the MainPage constructor arguments.
As the Silverlight will use as CWE app we don’t need to ask for Lync client (as we already has it).
public MainPage(IDictionary<string, string> _settings)
{
InitializeComponent();
_vm = new MainViewModel(_settings);
this.DataContext = _vm;
btnSend.Content = _vm.Settings.SendButtonText;
btnClear.Content = _vm.Settings.ClearButtonText;
lblLoading.Text = _vm.Settings.Loading;
LayoutRoot.FlowDirection = _vm.Settings.RTL
? FlowDirection.RightToLeft
: FlowDirection.LeftToRight;
txtPhoneNumbers.FlowDirection = FlowDirection.LeftToRight;
try
{
_conversation = (Conversation)LyncClient.GetHostingConversation();
if (_conversation == null)
return;
if (_conversation != null)
{
foreach (Participant participant in _conversation.Participants.Skip(1))
{
object[] endpoints = (object[])participant.Contact.GetContactInformation(ContactInformationType.ContactEndpoints);
foreach (ContactEndpoint phone in endpoints.Cast<ContactEndpoint>().
Where(phone => phone.Type == Microsoft.Lync.Model.ContactEndpointType.MobilePhone))
{
_vm.AddContact(phone.DisplayName);
}
}
if (string.IsNullOrEmpty(_vm.PhoneNumbers))
_vm.DisplayMessage(_vm.Settings.NoPhoneFoundMessage);
}
}
catch (Exception exception)
{
MessageBox.Show(exception.Message, "Error While GetHostingConversation", MessageBoxButton.OK);
}
}
ViewModel
ContactSearchResultListHandler – When you search a contact using Lync Contacts Search Control this handler will invoke, we need to extract the phone number of the selected contact from the list.
public void ContactSearchResultListHandler(SelectionChangedEventArgs e)
{
if (e.AddedItems.Count <= 0) return;
Microsoft.Lync.Controls.SearchResult searchResult = e.AddedItems[0] as Microsoft.Lync.Controls.SearchResult;
if (searchResult == null) return;
ContactModel contact = searchResult.Contact as ContactModel;
var mobilEndpoint =
contact.PresenceItems.Endpoints.FirstOrDefault(en => en.Type == ContactEndpointType.Mobile);
if (mobilEndpoint == null)
{
DisplayMessage(Settings.NoPhoneFoundMessage);
}
else
{
AddContact(mobilEndpoint.DisplayName);
}
}
AddContact - Will add the phone number to the PhoneNumbers property (displayed on the UI).
public void AddContact(string number)
{
number = Regex.Replace(number, @"[\D]", string.Empty).Trim();
if (string.IsNullOrEmpty(PhoneNumbers))
PhoneNumbers = number;
else if (!PhoneNumbers.Contains(number))
{
PhoneNumbers = string.Format(PhoneNumbers.EndsWith(";")
? "{0}{1}" : "{0};{1}", PhoneNumbers, number);
}
DisplayMessage(string.Empty);
}
Send SMS – Using both BackgroundWorker and ManualResetEvent to execute the send SMS message, this method will check either the settings are using Rest or Service and send the request based on the settings the user defined.
void _bg_DoWork(object sender, DoWorkEventArgs e)
{
var type = (WebRequestType)e.Argument;
SendSMSResponse response = new SendSMSResponse();
ManualResetEvent.Reset();
switch (type)
{
case WebRequestType.Service:
{
try
{
WebService ws = new WebService(Settings.ServiceUrl, Settings.ServiceMethod);
ws.ServiceResponseEvent += (s, args) =>
{
ManualResetEvent.Set();
response.IsError = !s;
response.Message = Settings.SuccessMessage;
};
ws.Params.Add(Settings.ServicePhonesParamName, PhoneNumbers);
ws.Params.Add(Settings.ServiceMessageParamName, SmsMessage);
ws.Invoke();
ManualResetEvent.WaitOne();
}
catch (Exception ex)
{
response.IsError = true;
response.Message = ex.Message;
}
finally
{
e.Result = response;
}
}
break;
case WebRequestType.Rest:
try
{
Uri uri = new Uri(string.Format(Settings.RestUrl, PhoneNumbers, SmsMessage));
WebClient client = new WebClient();
client.Headers["Content-Type"] = "text/plain;charset=utf-8";
client.OpenReadCompleted += (o, a) =>
{
ManualResetEvent.Set();
if (a.Error != null)
{
response.IsError = true;
response.Message = a.Error.Message;
return;
}
response.Message = Settings.SuccessMessage;
};
client.OpenReadAsync(uri);
ManualResetEvent.WaitOne();
}
catch (Exception ex)
{
response.IsError = true;
response.Message = ex.Message;
}
finally
{
e.Result = response;
}
break;
}
}
Install The Add in
Once we finished our logic and everything is ready we just need to change some registry keys so Lync will know out add in, let’s start with the custom command from the Context Menu, we need to launch out WPF and pass the contact argument and the location of the HTML file.
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\15.0\Lync\SessionManager\Apps\{3B3AAC0C-046A-4161-A44F-578B813E0BCF}]
"Name"="Send SMS"
"Path"="[ProgramFilesFolder][Manufacturer]\[ProductName]\SR.LyncSMS.App.exe /contactId:%contact-id% /htmlPath:"[ProgramFilesFolder][Manufacturer]\[ProductName]\LyncSMSContextAddinPage.html"
"ApplicationType"=dword:00000000
"SessionType"=dword:00000000
"Extensiblemenu"="MainWindowActions;MainWindowRightClick;ContactCardMenu;ConversationWindowContextual"
For CWE Silverlight application we need to specific where the Silverlight Html page locate.
[Install a CWE application in Lync SDK - https://msdn.microsoft.com/en-us/library/office/jj933101.aspx]
[HKEY_CURRENT_USER\Software\Microsoft\Communicator\ContextPackages\{310A0448-AF7C-49B0-9D8B-CC59A13E63E3}]
"DefaultContextPackage"="0"
"ExtensibilityApplicationType"="0"
"ExtensibilityWindowSize"="1"
"ExternalURL"="<a href="file:
"InternalURL"="<a href="file:
"Name"="Send SMS"
Run the setup, restart Lync and you should see both Send SMS from the Context Menu and CWE Window.
History