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 窗口中都看到“发送短信”选项。



