本章来学习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日