Lync Addin - 发送 SMS





5.00/5 (4投票s)
Lync 插件开发,如何使用单一代码库添加自定义上下文菜单和 CWE 应用程序。
- 下载 Lync_Setup.zip - 设置
- 下载 LyncSMSAddin-noexe.zip - 源代码
引言
我工作的许多公司都在使用 Lync 进行公司内外的沟通,与大多数即时通讯工具不同,Lync 不允许离线消息。这意味着当您尝试向处于离线状态的联系人发送消息时,您会收到以下消息:
由于用户不可用或离线,我们无法发送此消息。
因此,我决定提供一个解决方法(不,我不是在 Lync 中添加离线消息功能),该方法允许我直接从 Lync 发送短信消息(还会提取用户名和手机号码)。
* 发送短信需要支持该功能的服务器 *
上下文菜单 | 对话窗口扩展 (CWE) |
![]() |
![]() |
使用代码
在我们开始之前,请确保您拥有以下内容才能构建项目:
支持的操作系统软件
- Microsoft Windows Server 2008 R2
- Microsoft Windows 7 (64 位)
- Microsoft Windows 7 (32 位)
所需软件
- Microsoft Lync 2013 SDK
- Visual Studio 2013 或更高版本(如果您使用的是 Visual Studio 2012,则必须安装 Project Linker 2012 以支持共享项目)
- Microsoft Visual Studio 2013 安装程序项目
项目结构:
- Silverlight – Lync 2013 CWE 应用程序必须是 Silverlight
- WPF – 将从上下文菜单和桌面启动。
- Shared – 包含 Lync API 的类,需要在 Silverlight 和 Wpf 应用程序中进行编译。
- Common - 包含所有发送短信资产的可移植类库。
由于 CWE 和上下文菜单应用程序不同(Silverlight 和 WPF),我不得不重写两次 UI,但我使用了相同的代码库,通过 Shared 和 Portable 库。
辅助函数
HttpUtility.cs – 来自可移植库的原生编码 URL。
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;
// count them first
for (int i = 0; i < count; i++)
{
char ch = (char)bytes[offset + i];
if (ch == ' ')
cSpaces++;
else if (!IsSafe(ch))
cUnsafe++;
}
// nothing to expand?
if (!alwaysCreateReturnValue && cSpaces == 0 && cUnsafe == 0)
return bytes;
// expand not 'safe' characters into %XX, spaces to +s
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 - 主要设置文件,包含短信服务器 URL、请求方法等。
如果您的短信服务器基于 REST 或 SOAP,您可以在设置文件中定义它,将参数保留在 URL 中,例如:
http://www.freesmsservice.com/sendSMS?Phones={0}&Message={1}
应用程序会将消息和电话号码注入 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 = "http://www.demoservice.com/service.svc";
public string RestUrl = "http://www.demoservice.com/service?Phones={0}&Message={1}";
public string ServiceMethod;
public string ServicePhonesParamName;
public string ServiceMessageParamName;
public HttpMethod RestMethod { get; set; }
}
如果您不想更改代码并使用外部设置文件,可以编辑安装目录中的 HTML 文件,它将覆盖默认设置。(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}" />
<!--* Additonal Information *-->
<!-- WebRequestType -> Service or Rest-->
<!-- Service => clientaccesspolicy.xml required. Read more here: http://msdn.microsoft.com/en-us/library/cc645032(VS.95).aspx -->
<!-- RestMethod -> POST or GET-->
<!-- ServiceMethod -> String - Method Should Received Two Parameters (Phones, Message) -->
WPF 应用程序
当您从 Lync 上下文菜单调用外部应用程序时,您可以传递附加参数,例如联系人信息等。
[向 Lync 菜单添加自定义命令 - https://msdn.microsoft.com/EN-US/library/jj945535.aspx]
例如:Path="C:\\ExtApp1.exe /userId=%user-id% /contactId=%contact-id%"
我使用这些参数来传递 HTML 文件的位置(htmlPath)(可以在设置向导中更改位置)以及包含要发送短信的电话号码的contactId。
为了支持应用程序参数,我修改了App.xaml.cs 文件中的 OnStartup 方法。
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);
}
}
让我们转到 MainWindow.xaml.cs,这是 WPF 应用程序的核心,有两种初始化 ViewModel 的选项:使用包含参数的 HTML 文件或使用默认值。
构造函数中的第一行将调用 – DefineVMModel 方法来检查是否有 HTML 文件参数,如果有,它将解析到 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)
{
}
}
构造函数中的第二个操作将获取联系人电话号码(假设 Contacts 参数可用),这需要获取 Lync 客户端(此时您必须添加 Lync SDK)。
(请参阅内联注释中的其他数据)
try
{
client = LyncClient.GetClient();
//Making sure Lync is valid for operations
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(','))
{
//Contacts param can contain several contains contacts, for each we need to obtain the contact object.
var contact = client.ContactManager.GetContactByUri(contactSip.Replace("<", string.Empty).Replace(">", string.Empty));
contacts.Add(contact);
}
foreach (Contact contact in contacts)
{
//Once we have contact object we'll ask Lync for Contact Information and search only for phone of type MobilePhone.
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);
}
关于 UI,它非常简单,XAML 绑定到 ViewModel,我将在帖子后面讨论 VM。
使用 Lync SDK 允许我将 Lync 控件添加到我的 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
Silverlight 应用程序使用与 WPF 应用程序相同的概念,不同之处在于 Silverlight 在 MainPage 构造函数参数中自动接收来自 Html 文件的 InitParams。
由于 Silverlight 将用作 CWE 应用程序,因此我们无需请求 Lync 客户端(因为我们已经拥有它)。
public MainPage(IDictionary<string, string> _settings)
{
InitializeComponent();
_vm = new MainViewModel(_settings);
this.DataContext = _vm;
//_vm.MessageSent += VmOnMessageSent;
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); // "Contact doesn't have mobile number defined.";
}
}
catch (Exception exception)
{
MessageBox.Show(exception.Message, "Error While GetHostingConversation", MessageBoxButton.OK);
}
}
ViewModel
ContactSearchResultListHandler – 当您使用 Lync 联系人搜索控件搜索联系人时,将调用此处理程序,我们需要从列表中提取选定联系人的电话号码。
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);// "Contact doesn't have mobile number defined.";
}
else
{
AddContact(mobilEndpoint.DisplayName);
}
}
AddContact - 会将电话号码添加到 PhoneNumbers 属性(显示在 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 – 使用 BackgroundWorker 和 ManualResetEvent 来执行发送短信消息,此方法将检查设置是使用 Rest 还是 Service,并根据用户定义的设置发送请求。
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;
}
}
安装插件
完成逻辑并一切就绪后,我们只需更改一些注册表项,以便 Lync 了解我们的插件。让我们先从上下文菜单的自定义命令开始,我们需要启动我们的 WPF 并传递联系人参数和 HTML 文件的位置。
[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"
对于 CWE Silverlight 应用程序,我们需要指定 Silverlight HTML 页面的位置。
[在 Lync SDK 中安装 CWE 应用程序 - 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"="file:///[ProgramFilesFolder][Manufacturer]/[ProductName]/LyncSMSContextAddinPage.html"
"InternalURL"="file:///[ProgramFilesFolder][Manufacturer]/[ProductName]/LyncSMSContextAddinPage.html"
"Name"="Send SMS"
运行设置,重新启动 Lync,您应该会在上下文菜单和 CWE 窗口中都看到“发送短信”选项。