65.9K
CodeProject 正在变化。 阅读更多。
Home

Lync Addin - 发送 SMS

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2015 年 5 月 21 日

CPOL

4分钟阅读

viewsIcon

18507

downloadIcon

926

Lync 插件开发,如何使用单一代码库添加自定义上下文菜单和 CWE 应用程序。

引言

我工作的许多公司都在使用 Lync 进行公司内外的沟通,与大多数即时通讯工具不同,Lync 不允许离线消息。这意味着当您尝试向处于离线状态的联系人发送消息时,您会收到以下消息:
由于用户不可用或离线,我们无法发送此消息。

因此,我决定提供一个解决方法(不,我不是在 Lync 中添加离线消息功能),该方法允许我直接从 Lync 发送短信消息(还会提取用户名和手机号码)。

* 发送短信需要支持该功能的服务器 *

上下文菜单 对话窗口扩展 (CWE)

使用代码

在我们开始之前,请确保您拥有以下内容才能构建项目:

支持的操作系统软件

  • Microsoft Windows Server 2008 R2
  • Microsoft Windows 7 (64 位)
  • Microsoft Windows 7 (32 位)

所需软件

项目结构: 

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

历史

© . All rights reserved.