在《开源企业即时通讯和在线客服》中已介绍了Lesktop的桌面模式和Web模式,但是没有移动端。评论中 dotnetcms.org工作室 提到了LayIM,看了一下官网的演示和文档,如果用这套LayIM的移动端Lestop也可以轻松开发出移动端web版本。本文将说明如何接入LayIM移动端UI,同时对一些Lesktop的接口进行说明,作为接入其他前端UI的指引。 

移动端功能展示:

Lesktop开源IM移动端:接入LayIM移动端UI-LMLPHP Lesktop开源IM移动端:接入LayIM移动端UI-LMLPHPLesktop开源IM移动端:接入LayIM移动端UI-LMLPHPLesktop开源IM移动端:接入LayIM移动端UI-LMLPHPLesktop开源IM移动端:接入LayIM移动端UI-LMLPHP

源代码下载:https://files.cnblogs.com/files/lucc/IM3.1.zip 

源代码Git: https://github.com/luchuncheng/Lesktop.git

在线演示

移动端:http://im.luchuncheng.com/mobile.aspx

Lesktop开源IM移动端:接入LayIM移动端UI-LMLPHP

注册用户&内部人员

Web版:http://im.luchuncheng.com 

PC版下载:http://client.luchuncheng.com

客服平台(访客端)

Web版:http://service.luchuncheng.com

PC版http://im.luchuncheng.com/client.ashx?chatwith=4&embedcode=1&createaccount=true 
       (embedcode=1表示显示ID=1的客服嵌入代码指定的客服人员,chatwith=4表示启动和ID=4的客服人员对话窗口)

1、登录

接入的第一个步骤就是登录,登录界面非常简单,就是两个文本框和一个登录按钮,服务单只需要调用ServerImpl.Instance.Login即可:

int userid = AccountImpl.Instance.GetUserID(user);
// 仅验证不启动回话,重定向到default.aspx再启动回话
ServerImpl.Instance.Login("", Context, userid, false, null, false, 2);

最后第二个参数startSession=false,表示只是设置cookie不启动会话,移动端的login.aspx仅仅只是验证,登录后重定向到default.aspx再启动会话

最后一个参数device=2表示登录设备为移动端web版

2、初始化

LayIM初始化时需要好友,群组和分组等信息,因此登录完成后需要提供这些数据。在此之前先了解一下Lesktop的常用联系人功能:
Lesktop开源IM移动端:接入LayIM移动端UI-LMLPHP

如上图所示,Lesktop允许用户自己创建常用联系人分组,支持无限层级,用户可以将好友或内部人员添加到自建的任何层级的分组中。由于LayIM不支持多层次分组,所以在移动端中将所有常用联系人不分层级显示,如下图所示

Lesktop开源IM移动端:接入LayIM移动端UI-LMLPHP

除了常用联系人,还需要一个“我的好友”分组,用于显示已加自己为好友的注册用户。接下来需要了解几个和分组,好友和群组相关的接口:

(1) ServerImpl.Instance.CommonStorageImpl.GetCategories

GetCategories用于获取用户创建的所有分组,返回值是一个DataRowCollection,每一行包括5个列:

  

(2) ServerImpl.Instance.CommonStorageImpl.GetCategoryItems

GetCategoryItems获取和分组相关的所有联系人,群组和部门ID(移动端只用到联系人),返回值为DataRowCollection,每一行包括4个列:

(3) Category_CH.GetAccountInfos

GetCategoryItems只能获取和分组相关的所有联系人的ID,还需要调用GetAccountInfos才能获取到联系人的全部详细信息,返回值为AccountInfo数组,AccountInfo属性如下:

 (4) AccountImpl.Instance.GetVisibleUsersDetails

GetVisibleUsersDetails用于获取所有和指定用户相关的联系人和群组,包括所有由管理员创建的内部人员,已加自己为好友的注册用户,自己创建和加入的所有群组和自己创建或被拉进去的多人会话,这部分数据主要是为LayIM提供“我的好友”分组和群聊,返回值为一个Hashtable,每个项的值为AccountInfo。

(5)ServerImpl.Instance.GetCurrentUser

GetCurrentUser用户获取当前用户详细信息(AccountInfo)

以上5个接口已经获取到了所有LayIM初始化需要的数据,打包成json,“赋值”给页面的MobileInitParams全局变量即可:

namespace Core.Web
{
    public class Mobile_Default : System.Web.UI.Page
    {
        string init_params_ = "{}";

        protected void Page_Load(object sender, EventArgs e)
        {
            AccountInfo current_user = ServerImpl.Instance.GetCurrentUser(Context);
            if(current_user != null)
            {
                String sessionId = Guid.NewGuid().ToString().ToUpper();
                ServerImpl.Instance.Login(sessionId, Context, current_user.ID, false, DateTime.Now.AddDays(7), true, 2);

                DataRowCollection categories = ServerImpl.Instance.CommonStorageImpl.GetCategories(current_user.ID);

                DataRowCollection items = ServerImpl.Instance.CommonStorageImpl.GetCategoryItems(current_user.ID);
                Hashtable users = Category_CH.GetAccountInfos(items);

                AccountInfo[] visible_users = AccountImpl.Instance.GetVisibleUsersDetails(current_user.Name);

                init_params_ = Utility.RenderHashJson(
                    "Result", true,
                    "IsLogin", true,
                    "UserInfo", current_user.DetailsJson,
                    "SessionID", sessionId,
                    "CompanyInfo", ServerImpl.Instance.CommonStorageImpl.GetCompanyInfo(),
                    "Categories", categories,
                    "CategorieItems", items,
                    "CategorieUsers", users,
                    "VisibleUsers", visible_users
                );
            }
            else
            {
                Response.Redirect("login.aspx");
            }
        }

        public string InitParams
        {
            get { return init_params_; }
        }
    }
}

Lesktop开源IM移动端:接入LayIM移动端UI-LMLPHP  

页面加载后,LayIM_Init里面就可以通过MobileInitParams获取到这些数据,LayIM初始化参数请看官网文档,以下函数用于将Lesktop的数据转换成LayIM需要的格式:

// 获取分组和联系人
function GetFriends()
{
    var friends = [];
    for (var i = 0; i < window.MobileInitParams.Categories.length; i++)
    {
        var category = window.MobileInitParams.Categories[i];
        if (category.Type == 1)
        {
            // Type=1为常用联系人类别,将所有常用联系人类别(不分层次)显示为LayIM的分组
            var groupid = category.ID + 10000;
            var group = {
                "groupname": category.Name,
                "id": groupid.toString(),
                "online": 0,
                "list": []
            };
            var user_count = 0;
            var online_count = 0;
            for (var j = 0; j < window.MobileInitParams.CategorieItems.length; j++)
            {
                // 从CategorieItems中获取该分组所有联系人ID
                var item = window.MobileInitParams.CategorieItems[j];
                if (item.CategoryID == category.ID)
                {
                    // 通过联系人ID从CategorieUsers中获取联系人详细信息
                    var friend_info = window.MobileInitParams.CategorieUsers[item.ItemID.toString()];
                    if(friend_info != undefined)
                    {
                        group.list.push({
                            "username": friend_info.Nickname,
                            "id": friend_info.ID.toString(),
                            "avatar": Core.CreateHeadImgUrl(friend_info.ID, 150, false, friend_info.HeadIMG),
                            "sign": ""
                        });
                        user_count++;
                        if (friend_info.State == "Online")
                        {
                            online_count++;
                        }
                    }
                }
            }
            if (user_count > 0)
            {
                friends.push(group);
            }
        }
    }

    var grou_myfriend = {
        "groupname": "我的好友",
        "id": LayIMGroup_MyFriend,
        "online": 0,
        "list": []
    }

    var current_user = window.MobileInitParams.UserInfo;


    // 获取所有好友并显示到好友分组
    for (var i = 0; i < window.MobileInitParams.VisibleUsers.length; i++)
    {
        var user = window.MobileInitParams.VisibleUsers[i];
        if (user.Type == 0 && ((current_user.SubType == 1 && user.SubType == 0) || current_user.SubType == 0))
        {
            // 内部人员(SubType=1)显示注册用户并添加自己为好友的,不包括其他内部人员
            // 注册用户(SubType=0)显示添加自己为好友的其他注册用户和内部用户
            grou_myfriend.list.push({
                "username": user.Nickname,
                "id": user.ID.toString(),
                "avatar": Core.CreateHeadImgUrl(user.ID, 150, false, user.HeadIMG),
                "sign": ""
            });
        }
    }

    friends.push(grou_myfriend);

    friends.push({
        "groupname": "其他联系人",
        "id": LayIMGroup_Other,
        "online": 0,
        "list": []
    });

    return friends;
}
// 获取群聊
function GetGroups()
{
    // 获取所有群组和多人会话
    var groups = [];
    for (var i = 0; i < window.MobileInitParams.VisibleUsers.length; i++)
    {
        var user = window.MobileInitParams.VisibleUsers[i];
        if(user.Type == 1)
        {
            groups.push({
                "groupname": user.Nickname,
                "id": user.ID.toString(),
                "avatar": Core.CreateGroupImgUrl(user.HeadIMG, user.IsTemp)
            });
        }
    }
    return groups;
}  

3、接收消息

此次为了接入LayIM,加了一个全局委托Core.OnNewMessage,每当收到新消息是会调用该委托,如果需要监听新消息,只需要附加一个处理函数即可

function LayIM_OnNewMessage(msg)
{
}
// 监听新消息
Core.OnNewMessage.Attach(LayIM_OnNewMessage);

由于收到的消息可能是web或pc端发送的,包含LayIM消息面板不支持的富文本,因此需要先处理掉所有HTML tag,此外还需要处理文件标志([FILE:...])生成下载链接,完整代码如下:

function LayIM_ParseMsg(text)
{
    var newText = text;
    try
    {
        // 处理掉HTML开始TAG
        newText = text.toString().replace(
            /<([a-zA-Z0-9]+)([\s]+)[^<>]*>/ig,
            function (html, tag)
            {
                if (tag.toLowerCase() == "img")
                {
                    var filename = Core.GetFileNameFromImgTag(html);
                    if (filename != "")
                    {
                        // Lesktop服务器上的文件,重新加上分辨率限制参数,改为下载缩略图,链接到原图
                        var url = Core.CreateDownloadUrl(filename);
                        return String.format("a({0})[img[{0}&MaxWidth=450&MaxHeight=800]]", url);
                    }
                    else
                    {
                        // 外源图片,改成超链接,防止下载图片浪费流量
                        var src = Core.GetSrcFromImgTag(html);
                        return String.format("a({0})[{1}]", src, "&nbsp;图片&nbsp;");
                    }
                }
                return "";
            }
        )
        .replace(
            /\x5BFILE:([^\x5B\x5D]+)\x5D/ig,
            function (filetag, filepath)
            {
                // 提取文件消息,改为视频,音频或文件
                var path = unescape(filepath)
                var ext = Core.Path.GetFileExtension(path).toLowerCase();
                if (ext == ".mp4" || ext == ".mov")
                {
                    return String.format("video[{0}]", Core.CreateDownloadUrl(path), Core.Path.GetFileName(path));
                }
                else if (ext == "mp3")
                {
                    return String.format("audio[{0}]", Core.CreateDownloadUrl(path), Core.Path.GetFileName(path));
                }
                else
                {
                    return String.format("file({0})[{1}]", Core.CreateDownloadUrl(path), Core.Path.GetFileName(path));
                }
            }
        )
        .replace(
            /<([a-zA-Z0-9]+)[\x2F]{0,1}>/ig,
            function (html, tag)
            {
                // 清理<br/>等
                return "";
            }
        )
        .replace(
            /<\/([a-zA-Z0-9]+)>/ig,
            function (html, tag)
            {
                // 清理HTML结束TAG
                return "";
            }
        );
    }
    catch(ex)
    {
        newText += " ERROR:";
        newText += ex.message;
    }
    return newText;
}

function LayIM_OnNewMessage(msg)
{
    // msg.Sender, msg.Receiver只包括最基本的ID,Name,需重新获取详细信息
    var sender_info = Core.AccountData.GetAccountInfo(msg.Sender.ID);
    if (sender_info == null) sender_info = msg.Sender;
    var receiver_info = Core.AccountData.GetAccountInfo(msg.Receiver.ID);
    if (receiver_info == null) receiver_info = msg.Receiver;

    if (msg.Receiver.Type == 0)
    {
        // 私聊消息
        if (!LayIM_UserExists(sender_info.ID.toString()))
        {
            // 分组列表中不包括消息发送者,将发送者加入到其他联系人分组
            layim.addList({
                type: 'friend',
                "username": sender_info.Nickname,
                "id": sender_info.ID.toString(),
                "groupid": LayIMGroup_Other,
                "avatar": Core.CreateHeadImgUrl(sender_info.ID, 150, false, sender_info.HeadIMG),
                "sign": ""
            });
        }
        // 显示到LayIM消息面板
        layim.getMessage({
            username: sender_info.Nickname,
            avatar: Core.CreateHeadImgUrl(msg.Sender.ID, 150, false, sender_info.HeadIMG),
            id: msg.Sender.ID.toString(),
            type: "friend",
            cid: msg.ID.toString(),
            content: LayIM_ParseMsg(msg.Content)
        });
    }
    else if (msg.Receiver.Type == 1)
    {
        // 群消息
        if (!LayIM_GroupExists(receiver_info.ID.toString()))
        {
            // 群聊列表中不包括该群,加入到群聊中
            layim.addList({
                "type": "group",
                "groupname": receiver_info.Nickname,
                "id": receiver_info.ID.toString(),
                "avatar": Core.CreateGroupImgUrl(receiver_info.HeadIMG, receiver_info.IsTemp)
            });
        }
        // 显示到LayIM消息面板
        layim.getMessage({
            username: sender_info.Nickname,
            avatar: Core.CreateHeadImgUrl(msg.Sender.ID, 150, false, sender_info.HeadIMG),
            id: msg.Receiver.ID.toString(),
            type: "group",
            cid: msg.ID.toString(),
            content: LayIM_ParseMsg(msg.Content)
        });
    }
}

// 监听新消息
Core.OnNewMessage.Attach(LayIM_OnNewMessage);

4、发送消息

发送消息只需要调用服务端的WebIM.NewMessage方法即可,发送前,需要对消息进行预处理,把LayIM的标志(图片,文件和表情)转换成HTML,还需要调用Core.TranslateMessage,该函数用于将消息中的图片(<img ...>),文件([FILE:...])转换成服务端可以处理的附件,具体代码如下:

function LayIM_SendMsg_GetFileName(fileurl)
{
    var filename_regex = /FileName\=([^\s\x28\x29\x26]+)/ig;
    filename_regex.lastIndex = 0
    var ret = filename_regex.exec(fileurl);
    if (ret == null || ret.length <= 1)
    {
        return "";
    }
    return ret[1];
}

function LayIM_SendMsg(data)
{
    var msgdata = {
        Action: "NewMessage",
        Sender: parseInt(data.mine.id, 10),
        Receiver: parseInt(data.to.id, 10),
        DelTmpFile: 0,
        Content: ""
    };

    var content = data.mine.content;
    // 转换图片消息
    content = content.replace(
        /img\x5B([^\x5B\x5D]+)\x5D/ig,
        function(imgtext, src)
        {
            var filename = LayIM_SendMsg_GetFileName(src);
            return String.format('<img src="{0}">', Core.CreateDownloadUrl(filename));
        }
    );
    // 转换文件消息
    content = content.replace(
        /file\x28([^\x28\x29]+)\x29\x5B([^\x5B\x5D]+)\x5D/ig,
        function (filetext, fileurl, ope)
        {
            var path = unescape(LayIM_SendMsg_GetFileName(fileurl));
            return Core.CreateFileHtml([path]);
        }
    );
    // 将消息中的图片(<img ...>),文件([FILE:...])转换成服务端可以处理的附件
    content = Core.TranslateMessage(content, msgdata);
    // 转换表情
    content = content.replace(
        /face\[([^\s\[\]]+?)\]/g,
        function (face, face_type)
        {
            var face_file = LayIM_FaceToFile[face_type];
            if(face_file != undefined)
            {
                return String.format('<img src="{0}/{1}">', Core.GetUrl("layim/images/face"), face_file);
            }
        }
    );

    msgdata.Content = content;

    Core.SendCommand(
        function (ret)
        {
            var message = ret;
        },
        function (ex)
        {
            var errmsg = String.format("由于网络原因,消息\"{0}\"发送失败,错误信息:{1}", text, ex.Message);
        },
        Core.Utility.RenderJson(msgdata), "Core.Web WebIM", false
    );
}

5、异常状态处理

Lesktop有以下几种异常状态:

(1) 在其他浏览器或客户端登录,此时会收到强制下线通知(GLOBAL:OFFLINE)

(2) 服务端已升级,为简化服务端开发,Lesktop服务端要求客户端和前端都用对应的最新版本,不兼容旧版本。服务端网站升级后,升级前未退出重新连接上的客户端和web端都会收到不兼容异常通知(IncompatibleException)。PC需要重启升级,WEB端需要重登陆(发布版所有静态资源都放在名称为版本号的文件夹中,重登陆不会读取到缓存的资源)

(3) 验证身份异常,服务端网站可能会因为某种原因重新启动,此时会重新生成cookie加密密钥,会导致已在线的客户端无法从cookie获取身份信息,此时客户端会收到验证异常通知(UnauthorizedException)

移动端处理异常方法很简单,收到异常通知后,立刻重定向到offline.aspx页面,显示异常消息和重新登录按钮,如下图所示:
Lesktop开源IM移动端:接入LayIM移动端UI-LMLPHP  Lesktop开源IM移动端:接入LayIM移动端UI-LMLPHP 

至此,接入LayIM的工作就基本完成,前端代码基本都在mobile.js中。

因为LayIM不是开源的,因此git上不包括LayIM的源代码,需要自行购买,然后将src下的所有文件放到CurrentVersion/layim下:
Lesktop开源IM移动端:接入LayIM移动端UI-LMLPHP

10-08 21:38