基于XMPP实现即时通讯(四、聊天记录、聊天室)

本章来学习XMPP最后两个大的应用知识点:聊天记录和聊天室。

一、聊天记录

XMPP默认通过coredata存储聊天记录,先初始化并进行激活

//消息相关
var _xmppMessageArchinving: XMPPMessageArchiving!
var _xmppMessageStorage: XMPPMessageArchivingCoreDataStorage!

在activeXMPPModules方法中激活模块:

//消息相关
_xmppMessageStorage = XMPPMessageArchivingCoreDataStorage()
_xmppMessageArchinving = XMPPMessageArchiving(messageArchivingStorage: _xmppMessageStorage)
_xmppMessageArchinving.clientSideMessageArchivingOnly = true
//激活&添加代理
_xmppMessageArchinving.activate(_xmppStream)
_xmppMessageArchinving.addDelegate(self, delegateQueue: dispatch_get_main_queue())

功能实现,可以根据聊天界面下拉刷新的具体逻辑来实现一次获取多少条,或者是消息管理中心的全部记录,这里一次性获取全部聊天记录,不考虑数据量超大的情况:

  /**
     获取聊天记录

 - parameter userID:              如果为空,就是本地所有好友全部的聊天记录,这里friendid也可以为房间id,因为我们的房间信息发送走的也是正常聊天的_xmppMessageStorage存储
 - parameter getMessageListBlock: 回调
 */
func getMessageList(friendId: String?, getMessageListBlock: LGXMPP_GetMessageListBlock) {

    //如果是房间聊天,也可以从_xmppRoomStorage中获取数据库(xmppRoomStorage可以初始化为内存中的群消息对象,或者单独为群创建的coredata)
    let context = _xmppMessageStorage.mainThreadManagedObjectContext
    let entity = NSEntityDescription.entityForName("XMPPMessageArchiving_Message_CoreDataObject", inManagedObjectContext: context)

    let request = NSFetchRequest()
    request.entity = entity

    //全部查询出来
    //request.fetchLimit = 50         //一次最多查询50

    if friendId != nil{
        // 过滤内容,只找我与正要聊天的好友的聊天记录,注意:数据库内为小写
        let friendJidString = self.getChatJidString(friendId!)
        let predicate = NSPredicate(format: "bareJidStr = %@", friendJidString)
        request.predicate = predicate
    }

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {

        var results: [XMPPMessageArchiving_Message_CoreDataObject]?
        do {
            try results = context.executeFetchRequest(request) as? [XMPPMessageArchiving_Message_CoreDataObject]
        } catch let error as NSError {
            dispatch_async(dispatch_get_main_queue(), {
                return getMessageListBlock(messageList: nil, faildMsg: error.description)
            })
        }
        if (results != nil) && (results?.count != 0){
            var array = [XHMessage]()
            for object in results! {

                let oldMessage = self.getXHMessageFromXMPPMessage(object.message, messageSender: object.bareJidStr, isUserSendMessage: object.outgoing.intValue == 1 ? true : false, isHistory: true)

                if oldMessage.messageMediaType == XHBubbleMessageMediaType.Voice && oldMessage.voicePath == nil{
                    //语音没有从本地读到
                    //尝试在子线程去下载,下次拉记录时可以刷新出来,这次不再显示----也可以自己控制异步加载显示
                    LGTXCloudManager.shared.downloadFile(oldMessage.voiceUrl, sign: kTXCloud_File_Secret_ManyTime, sucessResult: nil, faildMsg: nil)
                    continue
                }

                oldMessage.avatar = UIImage(named: "_App_Icons")
                oldMessage.avatarUrl = "http://lorempixel.com/400/200/"
                array.append(oldMessage)
            }
            dispatch_async(dispatch_get_main_queue(), {
                return getMessageListBlock(messageList: array, faildMsg: nil)
            })
        }
        dispatch_async(dispatch_get_main_queue(), {
            return getMessageListBlock(messageList: nil, faildMsg: nil)
        })
    }
}

二、聊天室

聊天室也就是群聊,不过有一些业务权限上的区别,XMPP里面的聊天室是比较传统的聊天室业务,权限有:

拥有者 owner
管理员:admin 
成员:member  
黑名单:outcast  
游客:none(默认被邀请者为游客)

创建房间的人,默认就会成为owner,当owner邀请新用户加入房间时,如果不指定角色,默认为游客
房间拥有者可以改变房间配置、授予用户所有权和管理权限以及毁掉此房间。房间管理员可以禁止或授予用户权限和新的管理员权限。房间成员仅能允许用户加入房间(如果该房间配置为仅对成员开放)。同时房间被排除者是已禁止进入该房间的用户。XMPP中所说的主持人角色包括owner和admin,详见http://xmpp.org/extensions/xep-0045.html#associations
以上角色通过邀请时指定Affiliation来实现,如设置被邀请者的Affiliation为member表示给此被邀请用户成员角色
注意:只有owner和admin才有查询房间所有角色名单的权限,所以根据需求这里我们给被邀请者admin权限, 所有人都是主持人,但只有拥有者才可以销毁房间

为了使Demo简单明了,我们的业务逻辑是只允许用户在一个聊天室内聊天,要进入一个新的聊天室必须先离开原房间或销毁原房间。

1. 初始化

先初始化并进行激活:

var _xmppRoom: XMPPRoom?                                    //自己当前创建的聊天室
var _xmppRoomJid: XMPPJID?                                  //房间jid
var _xmppRoomOwnerMe = false                                //此房间是否是我创建的
var _xmppRoomStorage = XMPPRoomMemoryStorage()              //聊天室信息存储,只是放到内存中,也可根据业务情况用coredata方式的对象存储,如XMPPRoomCoreDataStorage
var _createChatRoomBlock: LGXMPP_CreateChatRoomBlock?       //创建房间回调
var _getChatRoomModeratorsBlock: LGXMPP_GetChatRoomModeratorsBlock? //获取房间主持人列表
var _xmppRoomCreateSuccess = false                          //房间创建成功
var _inChatRoom = false                                     //是否已经在聊天室内,因为本demo要确保同一时间只能在一个聊天室聊天

var _xmppMuc: XMPPMUC!                                      //房间邀请等数据对象

在activeXMPPModules方法中激活模块:

//聊天室相关
_xmppMuc = XMPPMUC()
_xmppStream.registerModule(_xmppMuc)
_xmppMuc.activate(_xmppStream)
_xmppMuc.addDelegate(self, delegateQueue: dispatch_get_main_queue())

声明房间对应的host,注意必须带”conference”(可以在Openfire控制台群组聊天中查看到对应域名):

let vHostRoom = "conference.JamieiMac.local"  

2. 创建、加入、邀请加入聊天室

注意:经过调试,创建房间方法调用后需要立即调用让自己肯定加入房间的方法,这样才能收到房间建立成功的回调,可能的原因是:默认自己肯定要加入房间,同时要加入房间才能收到房间建立成功的回调。
同时:当自己加入别人创建的房间时,也不会回调房间创建成功的方法,只会回调加入房间成功

 /**
     创建聊天室,并且直接加入此聊天室
 - parameter roomID:              聊天室ID,可以自己创建,也可根据需求向服务器申请
 - parameter ownerMe:             是否是自己创建的房间,他人邀请我加入房间时,我也要生成对应的房间对象
 - parameter createChatRoomBlock: 创建完房间的回调
 */
func createChatRoom(roomID: String, ownerMe: Bool, createChatRoomBlock: LGXMPP_CreateChatRoomBlock) {

    let roomJid = XMPPJID.jidWithString("\(roomID)@\(vHostRoom)")

    _xmppRoomOwnerMe = ownerMe
    if _xmppRoom != nil && _xmppRoom?.roomJID.user == roomJid.user && _xmppRoomCreateSuccess == true {
        //已经创建过了的房间
        createChatRoomBlock(isSuccess: true, faildMsg: nil)
        return
    }

    _xmppRoomCreateSuccess = false
    _createChatRoomBlock = nil
    _createChatRoomBlock = createChatRoomBlock

    _xmppRoom = XMPPRoom(roomStorage: _xmppRoomStorage, jid: roomJid, dispatchQueue: dispatch_get_main_queue())

    _xmppRoom?.activate(_xmppStream)
    _xmppRoom?.addDelegate(self, delegateQueue: dispatch_get_main_queue())

    //默认自己肯定要加入房间,同时要加入房间才能收到房间建立成功的回调
    self.joinNowChatRoom(_userId!)
}

/**
 加入聊天室
 - parameter nickName: 聊天室内的个人昵称
 */
func joinNowChatRoom(nickName: String) {
    _xmppRoom!.joinRoomUsingNickname(nickName, history: nil)
}

  /**
 邀请新人进入聊天室
 - parameter friendId: 好友ID(通常只能邀请自己的好友)
 */
func inviteUserToChatRoom(friendId: String) {
    let friendJidString = self.getChatJidString(friendId)
    let friendJID = XMPPJID.jidWithString(friendJidString)
    _xmppRoom!.inviteUser(friendJID, withMessage: "\(_userId!)邀请您加入群")
    //只有owner和admin才有查询房间所有角色名单的权限,所以根据需求这里我们给被邀请者admin权限, 这样所有人都是主持人,但只有拥有者才可以销毁房间
    _xmppRoom!.editRoomPrivileges([XMPPRoom.itemWithAffiliation("admin", jid: friendJID)])
}

对应回调:

func xmppRoomDidCreate(sender: XMPPRoom!) {

       DPrintln("房间创建成功 \(sender)")

       //设置房间默认配置属性
       _xmppRoom!.configureRoomUsingOptions(nil)


       _xmppRoomCreateSuccess = true
       if _createChatRoomBlock != nil{
           _createChatRoomBlock!(isSuccess: true, faildMsg: nil)
           _createChatRoomBlock = nil
       }
   }

   func xmppRoomDidJoin(sender: XMPPRoom!) {
       DPrintln("加入房间成功\(sender)")

       _inChatRoom = true

       //当加入已创建聊天室时,不会回调xmppRoomDidCreate,所以在此进行回调处理
       _xmppRoomCreateSuccess = true
       if _createChatRoomBlock != nil{
           _createChatRoomBlock!(isSuccess: true, faildMsg: nil)
           _createChatRoomBlock = nil
       }
   }

3. 查询聊天室相关信息

可以根据需求在加入聊天室成功后调用以下方法,但要注意,刚加入时去查询有可能会返回来空数组,这时可以尝试延时几秒去请求查询

_xmppRoom.fetchConfigurationForm    //查询聊天室配置
_xmppRoom.fetchBanList              //查询聊天室黑名单角色清单
_xmppRoom.fetchMembersList          //查询聊天室成员角色清单
_xmppRoom.fetchModeratorsList       //查询聊天室主持人角色清单

对应以下回调:

// MARK:  --------------------------------  聊天室回调 -- 信息查询  -------------------------------
func xmppRoom(sender: XMPPRoom!, didFetchBanList items: [AnyObject]!) {
    DPrintln("收到本群/房间 禁止人员 名单 \(items)")
}

func xmppRoom(sender: XMPPRoom!, didFetchMembersList items: [AnyObject]!) {
    DPrintln("收到本群/房间 所有成员角色名单 \(items)")

}

func xmppRoom(sender: XMPPRoom!, didFetchConfigurationForm configForm: DDXMLElement!) {
    DPrintln("获取到了聊天室配置属性\(configForm)")
}

func xmppRoom(sender: XMPPRoom!, didFetchModeratorsList items: [AnyObject]!) {
    DPrintln("收到本群/房间 主持人员/管理人员  名单 \(items)")
}

func xmppRoom(sender: XMPPRoom!, didNotFetchBanList iqError: XMPPIQ!) {
    DPrintln("查询失败,无法收到本群/房间 禁止人员 名单")
}

func xmppRoom(sender: XMPPRoom!, didNotFetchMembersList iqError: XMPPIQ!) {
    DPrintln("查询失败,无法收到本群/房间 所有人员 名单")
}

func xmppRoom(sender: XMPPRoom!, didNotFetchModeratorsList iqError: XMPPIQ!) {
    DPrintln("查询失败,无法收到本群/房间 主持人员/管理人员  名单")
}

还有其它设置聊天室配置是否成功等相关回调,可以根据需求添加。
加入聊天室成功后,也可以从本地数据库中查询聊天室人员信息,本Demo采用的是此方法:

/**
 获取群内所有人的userid清单(即jid的user)
 */
func getRoomAllOccupantsList() -> [String]? {
    if _xmppRoomStorage.occupants() == nil{
        return nil
    }
    var idArray = [String]()
    for occupantStorageObject in _xmppRoomStorage.occupants(){
        let jidString = occupantStorageObject.realJID().user
        idArray.append(jidString)
    }
    return idArray
}

4. 收到邀请、离开、销毁聊天室

聊天室邀请来自XMPPMUCDelegate的回调:

func xmppMUC(sender: XMPPMUC!, roomJID: XMPPJID!, didReceiveInvitation message: XMPPMessage!) {
        DPrintln("收到聊天室邀请")

        let roomName = message.attributeForName("from").stringValue()
        let x = message.elementForName("x") as DDXMLElement
        let invite = x.elementForName("invite")
        let fromUser = invite.attributeForName("from").stringValue()
        let reason = invite.elementForName("reason").stringValue()

        _xmppRoomJid = roomJID  //记录要进入的房间id
        let alert = UIAlertView(title: "来自\(fromUser)的聊天室邀请", message: "\(reason),是否加入\(roomName)?", delegate: self, cancelButtonTitle: "拒绝", otherButtonTitles: "加入")
        alert.tag = vJoinGroupAlertTag
        alert.show()
 }

然后在UIAlertView delegate中添加以下内容

...
 else if alertView.tag == vJoinGroupAlertTag{
            if buttonIndex == 1{
                DPrintln("同意加入群")
                if _inChatRoom == true && _xmppRoom != nil{
                    //或者此处让用户选择直接退出当前房间的逻辑也可
                    Tools.shared.showAlertViewAndDismissDefault("请先退出当前房间", message: "同一时刻你只能加入一个房间")
                    return
                }

                weak var weakSelf = self
                //创建此群对象及相关代理
                self.createChatRoom(_xmppRoomJid!.user, ownerMe: false, createChatRoomBlock: { (isSuccess, faildMsg) in
                    dispatch_async(dispatch_get_main_queue(), {

                        if isSuccess{
                           //以通知的方式将房间完整jid传给对应的界面去跳转或刷新UI
                          //需要群人员清单时通过上面的getRoomAllOccupantsList()方法获取
                           NSNotificationCenter.defaultCenter().postNotificationName("joinChatRoom", object: weakSelf!._xmppRoomJid!.bareJID().full(), userInfo: nil)
                        }
                    })
                })
            }else{
                DPrintln("")
            }
        }
    ...

离开、销毁聊天室:

 /**
     离开当前房间
     */
func levaRoom() {

    if _xmppRoom != nil{
        //如果是自己创建的房间,直接销毁此房间
        if _xmppRoomOwnerMe{
            _xmppRoom?.destroyRoom()

        }
        else{
            _xmppRoom!.leaveRoom()
        }
        _inChatRoom = false
    }
}

对应相关的回调:

func xmppRoomDidLeave(sender: XMPPRoom!) {
    DPrintln("退出房间成功\(sender)")
    _xmppRoom = nil
}

func xmppRoomDidDestroy(sender: XMPPRoom!) {
    DPrintln("房间已经销毁\(sender)")
    _xmppRoom = nil
}

func xmppRoom(sender: XMPPRoom!, occupantDidJoin occupantJID: XMPPJID!, withPresence presence: XMPPPresence!) {
    DPrintln("有新人加入房间\(occupantJID)")
}

func xmppRoom(sender: XMPPRoom!, occupantDidLeave occupantJID: XMPPJID!, withPresence presence: XMPPPresence!) {
    DPrintln("有新人离开房间\(occupantJID)")
}

func xmppRoom(sender: XMPPRoom!, occupantDidUpdate occupantJID: XMPPJID!, withPresence presence: XMPPPresence!) {
    DPrintln("房间有人更新了个人状态\(occupantJID)")
}

5. 聊天室消息

聊天室发送消息和 1对1聊天发送消息 调用方法相同,只是发出的Jid为房间id
聊天室消息接收如下:

// MARK:  --------------------------------  聊天室回调 -- 收到信息  -------------------------------
func xmppRoom(sender: XMPPRoom!, didReceiveMessage message: XMPPMessage!, fromOccupant occupantJID: XMPPJID!) {
    DPrintln("收到信息, 来自房间 \(sender), 内容:\(message) )")

    let messageString = message.stringValue()

    //群聊时要将自己的信息排除,因为发出去的信息还会回传给自己
    let fromID = occupantJID.full().lastPathComponent
    if !messageString.isEmpty && fromID.lowercaseString != _userId?.lowercaseString{

        let newMessage = self.getXHMessageFromXMPPMessage(message, messageSender: message.from().full(), isUserSendMessage: false, isHistory: false)
        //同1对1接收消息一样,走同样的通知
        NSNotificationCenter.defaultCenter().postNotificationName(kXMPPNewMessage, object: newMessage, userInfo: nil)
    }
    else{
        DPrintln("收到其它类型消息/非正常消息/回执等")
    }
}

聊天室消息记录和1对1获取消息记录一样,查询的Jid为房间iD即可,也可能通过_xmppRoomStorage去从内存或单独的群数据库中读取。

好了,整个XMPP的内容都学习了一次,剩下注册、查询房间列表等小的功能点,大家自己实现哈,有问题也可以找我交流学习哈,下章会将Demo给出。


作者 @代码书生
2016 年 08月 02日

尘满面,鬓如霜,Bug多多岂不白忙?重敏捷,保质量,Case重重亦可远航。^.^