var AV = require('leancloud-storage');
// 以用户名和密码登录内建账户系统
AV.User.logIn('username', 'password').then(function(user) {
// 直接使用 LCUser 实例登录即时通讯服务
return realtime.createIMClient(user);
}).catch(console.error.bind(console));
_ = LCUser.logIn(username: "username", password: "password") { (result) in
switch result {
case .success(object: let user):
do {
let client = try IMClient(user: user)
client.open(completion: { (result) in
// 执行其他逻辑
})
} catch {
print(error)
}
case .failure(error: let error):
print(error)
}
}
/// <summary>
/// Updates the role of a member of this conversation.
/// </summary>
/// <param name="memberId">The member to update.</param>
/// <param name="role">The new role of the member.</param>
/// <returns></returns>
public async Task UpdateMemberRole(string memberId, string role);
/**
* 更新成员的角色信息
* @param memberId 成员的 clientId
* @param role 角色
* @param callback 结果回调函数
*/
public void updateMemberRole(final String memberId, final ConversationMemberRole role, final LCIMConversationCallback callback);
/// Updating role of the member in the conversaiton.
///
/// - Parameters:
/// - role: The role will be updated.
/// - memberID: The ID of the member who will be updated.
/// - completion: Result of callback.
/// - Throws: If role parameter is owner, throw error.
public func update(role: MemberRole, ofMember memberID: String, completion: @escaping (LCBooleanResult) -> Void) throws
/// - role: The role will be updated.
/// - memberId: The ID of the member who will be updated.
Future<void> updateMemberRole({String role, String memberId})
/// Fetching the table of member infomation in the conversation.
/// The result will be cached by the property `memberInfoTable`.
///
/// - Parameter completion: Result of callback.
public func fetchMemberInfoTable(completion: @escaping (LCBooleanResult) -> Void)
/// The table of member infomation.
public var memberInfoTable: [String : MemberInfo]? { get }
/**
* 获取当前对话的所有角色信息
* @param offset 查询结果的起始点
* @param limit 查询结果集上限
* @param callback 结果回调函数
*/
public void getAllMemberInfo(int offset, int limit, final LCIMConversationMemberQueryCallback callback);
/// <summary>
/// Gets all member roles.
/// </summary>
/// <returns></returns>
public async Task<ReadOnlyCollection<LCIMConversationMemberInfo>> GetAllMemberInfo();
/// Get infomation of one member in the conversation.
///
/// - Parameters:
/// - memberID: The ID of the member.
/// - completion: Result of callback.
public func getMemberInfo(by memberID: String, completion: @escaping (LCGenericResult<MemberInfo?>) -> Void)
/// <summary>
/// Gets the role of a specific member.
/// </summary>
/// <param name="memberId">The member to query.</param>
/// <returns></returns>
public async Task<LCIMConversationMemberInfo> GetMemberInfo(string memberId);
/// <summary>
/// Mutes members of this conversation.
/// </summary>
/// <param name="clientIds">Member list.</param>
/// <returns></returns>
public async Task<LCIMPartiallySuccessResult> MuteMembers(IEnumerable<string> clientIds);
/// <summary>
/// Unmutes members of this conversation.
/// </summary>
/// <param name="clientIdList">Member list.</param>
/// <returns></returns>
public async Task<LCIMPartiallySuccessResult> UnmuteMembers(IEnumerable<string> clientIds);
/// <summary>
/// Queries muted members.
/// </summary>
/// <param name="limit">Limits the number of returned results.</param>
/// <param name="next">Can be used for pagination with the limit parameter.</param>
/// <returns></returns>
public async Task<LCIMPageResult> QueryMutedMembers(int limit = 10, string next = null);
/**
* 将部分成员禁言
* @param memberIds 成员列表
* @param callback 结果回调函数
*/
public void muteMembers(final List<String> memberIds, final LCIMOperationPartiallySucceededCallback callback);
/**
* 将部分成员解除禁言
* @param memberIds 成员列表
* @param callback 结果回调函数
*/
public void unmuteMembers(final List<String> memberIds, final LCIMOperationPartiallySucceededCallback callback);
/**
* 查询被禁言的成员列表
* @param offset 查询结果的起始点
* @param limit 查询结果集上限
* @param callback 结果回调函数
*/
public void queryMutedMembers(int offset, int limit, final LCIMConversationSimpleResultCallback callback);
/// Muting members in the conversation.
///
/// - Parameters:
/// - members: The members will be muted.
/// - completion: Result of callback.
/// - Throws: When parameter `members` is empty.
public func mute(members: Set<String>, completion: @escaping (MemberResult) -> Void) throws
/// Unmuting members in the conversation.
///
/// - Parameters:
/// - members: The members will be unmuted.
/// - completion: Result of callback.
/// - Throws: When parameter `members` is empty.
public func unmute(members: Set<String>, completion: @escaping (MemberResult) -> Void) throws
/// Get the muted members in the conversation.
///
/// - Parameters:
/// - limit: Count limit.
/// - next: Offset.
/// - completion: Result of callback.
/// - Throws: When parameter `limit` out of range.
public func getMutedMembers(limit: Int = 50, next: String? = nil, completion: @escaping (LCGenericResult<MutedMembersResult>) -> Void) throws
/// Check if one member has been muted in the conversation.
///
/// - Parameters:
/// - ID: The ID of member.
/// - completion: Result of callback.
public func checkMuting(member ID: String, completion: @escaping (LCGenericResult<Bool>) -> Void)
/// - members: The members will be muted.
Future<MemberResult> muteMembers({Set<String> members})
/// - members: The members will be unmuted.
Future<MemberResult> unmuteMembers({Set<String> members})
/// Get the muted members in the conversation.
///
/// [limit]'s default is `50`, should not more than `100`.
/// [next]'s default is `null`.
///
/// Returns a list of members.
Future<QueryMemberResult> queryMutedMembers({int limit = 50, String next})
/// <summary>
/// Adds members to the blocklist of this conversation.
/// </summary>
/// <param name="clientIds">Member list.</param>
/// <returns></returns>
public async Task<LCIMPartiallySuccessResult> BlockMembers(IEnumerable<string> clientIds);
/// <summary>
/// Removes members from the blocklist of this conversation.
/// </summary>
/// <param name="clientIds">Member list.</param>
/// <returns></returns>
public async Task<LCIMPartiallySuccessResult> UnblockMembers(IEnumerable<string> clientIds);
/// <summary>
/// Queries blocked members.
/// </summary>
/// <param name="limit">Limits the number of returned results.</param>
/// <param name="next">Can be used for pagination with the limit parameter.</param>
/// <returns></returns>
public async Task<LCIMPageResult> QueryBlockedMembers(int limit = 10, string next = null);
/**
* 将部分成员加入黑名单
* @param memberIds 成员列表
* @param callback 结果回调函数
*/
public void blockMembers(final List<String> memberIds, final LCIMOperationPartiallySucceededCallback callback);
/**
* 将部分成员从黑名单移出来
* @param memberIds 成员列表
* @param callback 结果回调函数
*/
public void unblockMembers(final List<String> memberIds, final LCIMOperationPartiallySucceededCallback callback);
/**
* 查询黑名单的成员列表
* @param offset 查询结果的起始点
* @param limit 查询结果集上限
* @param callback 结果回调函数
*/
public void queryBlockedMembers(int offset, int limit, final LCIMConversationSimpleResultCallback callback);
/// Blocking members in the conversation.
///
/// - Parameters:
/// - members: The members will be blocked.
/// - completion: Result of callback.
/// - Throws: When parameter `members` is empty.
public func block(members: Set<String>, completion: @escaping (MemberResult) -> Void) throws
/// Unblocking members in the conversation.
///
/// - Parameters:
/// - members: The members will be unblocked.
/// - completion: Result of callback.
/// - Throws: When parameter `members` is empty.
public func unblock(members: Set<String>, completion: @escaping (MemberResult) -> Void) throws
/// Get the blocked members in the conversation.
///
/// - Parameters:
/// - limit: Count limit.
/// - next: Offset.
/// - completion: Result of callback.
/// - Throws: When limit out of range.
public func getBlockedMembers(limit: Int = 50, next: String? = nil, completion: @escaping (LCGenericResult<BlockedMembersResult>) -> Void) throws
/// Check if one member has been blocked in the conversation.
///
/// - Parameters:
/// - ID: The ID of member.
/// - completion: Result of callback.
public func checkBlocking(member ID: String, completion: @escaping (LCGenericResult<Bool>) -> Void)
/// - members: The members will be blocked.
Future<MemberResult> blockMembers({Set<String> members})
/// - members: The members will be un unblocked.
Future<MemberResult> unblockMembers({Set<String> members})
/// Get the blocked members in the conversation.
///
/// [limit]'s default is `50`, should not more than `100`.
/// [next]'s default is `null`.
///
/// Returns a list of members.
Future<QueryMemberResult> queryBlockedMembers({int limit = 50, String next})
注意这里对黑名单操作的结果与禁言操作一样,是 部分成功结果。
用户被加入黑名单之后,就被从对话的成员中移除出去了,以后都无法再接收到对话里面的新消息,并且除非解除黑名单,其他人都无法再把 ta 加为对话成员了。
do {
try client.createChatRoom(name: "聊天室", attributes: nil) { (result) in
switch result {
case .success(value: let chatRoom):
print(chatRoom)
case .failure(error: let error):
print(error)
}
}
} catch {
print(error)
}
do {
let query = client.conversationQuery
try query.where("tr", .equalTo(true))
try query.findConversations { (result) in
switch result {
case .success(value: let conversations):
guard conversations is [IMChatRoom] else {
return
}
case .failure(error: let error):
print(error)
}
}
} catch {
print(error)
}
do {
chatRoom.getOnlineMembersCount { (result) in
switch result {
case .success(count: let count):
print(count)
case .failure(error: let error):
print(error)
}
}
} catch {
print(error)
}
do {
let message = IMTextMessage(text: "现在比分是 0:0,下半场中国队肯定要做出人员调整")
try chatRoom.send(message: message, priority: .high) { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
}
} catch {
print(error)
}
do {
try client.createTemporaryConversation(clientIDs: ["Jerry", "William"], timeToLive: 3600) { (result) in
switch result {
case .success(value: let tempConversation):
print(tempConversation)
case .failure(error: let error):
print(error)
}
}
} catch {
print(error)
}
do {
try client.createTemporaryConversation(clientIDs: ["Jerry", "William"], timeToLive: 3600) { (result) in
switch result {
case .success(value: let tempConversation):
print(tempConversation)
case .failure(error: let error):
print(error)
}
}
} catch {
print(error)
}
三,安全与签名、黑名单和权限管理、玩转聊天室和临时对话
本章导读
在前一篇消息收发的更多方式,离线推送与消息同步,多设备登录中,我们演示了与消息相关的更多特殊需求的实现方法,现在,我们会更进一步,从系统安全和成员权限管理的角度,给大家详细说明:
安全与签名
即时通讯服务有一大特色就是让应用账户系统和聊天服务解耦,终端用户只需要登录应用账户系统就可以直接使用即时通讯服务,同时从系统安全角度出发,我们还提供了第三方操作签名的机制来保证聊天通道的安全性。
该机制的工作架构是,在客户端和即时通讯云端之间,增加应用自己的鉴权服务器(也就是即时通讯服务之外的「第三方」),在客户端开始一些有安全风险的操作命令(如登录聊天服务、建立对话、加入群组、邀请他人等)之前,先通过鉴权服务器获取签名,之后即时通讯云端会依据它和第三方鉴权服务之间的协议来验证该签名,只有附带有效签名的请求才会被执行,非法请求全部会被阻止下来。
使用操作签名可以保证聊天通道的安全,这一功能默认是关闭的,可以在 云服务控制台 > 即时通讯 > 设置 > 即时通讯设置 中进行开启:
开发者可根据实际需要进行选择。一般来说,登录认证 是最基本的安全机制,我们强烈建议开发者开启登录认证。
SignatureFactory
的实现,并携带用户信息和用户行为(登录、新建对话或群组操作)请求签名;签名采用 HMAC-SHA1 算法,输出字节流的十六进制字符串(hex dump)。针对不同的请求,开发者需要拼装不同组合的字符串,加上 UTC timestamp 以及随机字符串作为签名的消息(参见后续格式说明)。总体上,签名就是使用特定的密钥(在这里我们使用应用的 Master Key),对输入的消息(即「签名的消息」)进行哈希计算,得到一串十六进制的字符串,这就是最终的「签名」。
对于使用
LCUser
的应用,可使用 REST API 获取登录签名进行登录认证。签名格式说明
下面我们详细说明一下不同操作的签名消息格式。
用户登录签名
签名的消息格式如下,注意
clientid
与timestamp
之间是两个冒号:appid
clientid
clientId
。timestamp
nonce
开发者可以实现自己的
SignatureFactory
,调用远程服务器的签名接口获得签名。如果你没有自己的服务器,可以直接在云引擎上通过 网站托管 来实现自己的签名接口。在移动应用中直接进行签名的做法 非常危险,它可能导致你的 Master Key 泄漏。签名的有效期是 6 个小时,强制下线后签名立即失效。 签名失效不影响当前在线的 client。
开启对话签名
新建一个对话的时候,签名的消息格式为:
appid
、clientid
、timestamp
和nonce
的含义 同上。sorted_member_ids
是以半角冒号(:
)分隔、升序排序 的clientId
,即邀请参与该对话的成员列表。群组功能的签名
在群组功能中,我们对 加群、邀请 和 踢出群 这三个动作也允许加入签名,签名的消息格式是:
appid
、clientid
、sorted_member_ids
、timestamp
和nonce
的含义同上。对创建群的情况,这里sorted_member_ids
是空字符串。convid
是此次行为关联的对话 ID。action
是此次行为的动作,invite
表示加群和邀请,kick
表示踢出群。查询聊天记录的签名
各参数的含义同上。
注意,此签名仅用于通过 REST API 查询历史消息,客户端 SDK 不适用。
黑名单的签名
由于黑名单有两种情况,所以签名的消息格式也有两种:
client
对conversation
action
是此次行为的动作,client-block-conversations
表示添加黑名单,client-unblock-conversations
表示取消黑名单。conversation
对client
action
是此次行为的动作,conversation-block-clients
表示添加黑名单,conversation-unblock-clients
表示取消黑名单。sorted_member_ids
同上。云引擎签名范例
为了帮助开发者理解云端签名的算法,我们开源了一个用「Node.js + 云引擎」实现签名的云端,供开发者学习和使用:即时通讯云引擎签名 Demo。
客户端如何支持操作签名
上面的签名算法,都是对第三方鉴权服务器如何进行签名的协议说明,在开启了操作签名的前提下,客户端这边的使用流程需要进行相应的改变,增加请求签名的环节,才能让整套机制顺利运行起来。
即时通讯 SDK 为每一个
AVIMClient
实例都预留了一个Signature
工厂接口,这个接口默认不设置就表示不使用签名,启动签名的时候,只需要在客户端实现这一接口,调用远程服务器的签名接口获得签名,并把它绑定到AVIMClient
实例上即可:需要强调的是:开发者切勿在客户端直接使用 Master Key 进行签名操作,因为 Master Key 一旦泄露,会造成应用的数据处于高危状态,后果不容小视。因此,强烈建议开发者将签名的具体代码托管在安全性高稳定性好的云端服务器上(例如云引擎)。"
内建账户系统(User)的签名机制
User
是存储服务提供的默认账户系统,对于使用了它来完成用户注册、登录的产品来说,终端用户通过User
账户系统的登录认证之后,转到即时通讯服务上,是无需再进行登录签名操作的。 使用User
账号系统登录即时通讯服务的示例如下:内置账户系统与即时通讯服务可以共享登录签名信息,这里我们直接用
logIn
成功之后的LCUser
实例来创建IMClient
,在即时通讯服务的用户登录环节,云端会自动关联账户系统来确认用户身份的合法性,这样可以省掉 SDK 向第三方申请登录签名的操作,进一步简化开发流程。IMClient
完成即时通讯系统登录之后,其他功能的使用就和之前的介绍没有任何区别了。权限管理与黑名单
第三方鉴权是一种服务端对全局进行控制的机制,具体到单个对话的群组,例如开放聊天室,出于产品运营的需求,我们还需要对成员权限进行区分,以及允许管理员来限时/永久屏蔽部分用户。下面我们详细说明一下这样的需求该如何实现。
设置成员权限
「成员权限」是指将对话内成员划分成不同角色,实现类似 QQ 群管理员的效果。使用这个功能需要在 云服务控制台 > 即时通讯 > 设置 > 即时通讯设置 中开启「对话成员属性功能(成员角色管理功能)」。
目前系统内的角色与管理功能的对应关系:
Owner
Manager
Member
角色的操作权限大小是按照
Owner
->Manager
->Member
的顺序逐级递减的,高级别的角色可以修改低级别角色的权限,但反过来的修改是不允许的。同时,对于加人和踢人的操作,在前面文档中我们可以看到,是所有成员都可以执行的操作,在成员角色管理功能开启之后,就变成Owner
和Manager
专属的功能的,普通成员发起这两种请求都会报错。一个对话的
Owner
是不可变更的,我们 SDK 提供了Conversation#updateMemberRole
方法,支持把一个终端用户在Manager
和Member
之间切换角色:获取成员权限
Conversation
对象提供了两种方法来获取成员权限信息:Conversation#getAllMemberInfo()
可用来获取所有成员的权限信息Conversation#getMemberInfo(memberId)
可用来获取指定成员的权限信息这两类函数的返回值都是包含
<ConversationId, MemberId, ConversationMemberRole>
信息的三元组(数组)。让部分用户禁言
Owner
和Manager
作为聊天群组管理员的权限之一,就是能够让部分用户禁言。被禁言的用户,只能接收群组里面的消息,而不能再往外发送消息,否则会报错。LCIMConversation
类提供了对成员进行禁言操作的相关方法:注意这里对用户禁言/解除禁言的结果与以往的操作结果不一样,这里是 部分成功结果,里面包含三部分数据:
error
/exception
,表示整体是否成功。如果整体操作失败,这里会有异常信息返回,此时不必再看下面两部分结果。successfulClientIds
,表示操作成功了的成员 ID 列表。failedIds
,表示所有操作失败了的成员信息,以List<ReasonString, List<ClientId>>
的形式列出了所有的失败原因以及对应的成员 ID 列表。禁言的通知事件
管理员把部分用户禁言之后,即时通讯服务端会把这一事件下发给该群组里面的所有成员。
黑名单
「黑名单」功能可以实现类似微信「屏蔽」的效果,目前分为两大类
使用这个功能需要在 云服务控制台 > 即时通讯 > 设置 > 即时通讯设置 中开启「黑名单功能」。
LCIMConversation
类提供了对对话黑名单进行操作的方法:用户被加入黑名单之后,就被从对话的成员中移除出去了,以后都无法再接收到对话里面的新消息,并且除非解除黑名单,其他人都无法再把 ta 加为对话成员了。
黑名单的通知事件
管理员把部分用户加入黑名单之后,即时通讯服务端会把这一事件下发给该群组里面的所有成员。
屏蔽某用户发送的消息
还有一种场景是某个用户不希望收到特定用户发来的消息。这可以通过即时通讯 hook 函数实现,详见《即时通讯开发指南》第四篇。
玩转直播聊天室
在即时通讯服务总览中,我们比较了不同的业务场景与对话类型,现在就来看看如何使用「聊天室」完成一个直播弹幕的需求。
创建聊天室
IMClient
提供了专门的createChatRoom
方法来创建聊天室:在创建聊天室的时候,开发者可以指定聊天室的名字和附加属性(非必须),与创建普通对话的接口相比,有如下差异:
members
是没有意义的unique
标志也是没有意义的(云端无需根据成员 ID 来去重)查找聊天室
在即时通讯开发指南第一篇中,我们已经了解了构造复杂条件来查询对话的方法,
ConversationsQuery
依然适用于查询聊天室,只需要添加transient = true
的限制条件即可。加入和离开聊天室
查询到聊天室之后,加入和离开聊天室与普通对话的对应接口没有区别,详细请参考《即时通讯开发指南》第一篇《多人群聊》。
在成员管理与变更通知方面,聊天室与普通对话的最大区别就是:
另外,也请注意 聊天室也不支持离线推送通知、离线消息同步、消息回执等功能。
查询成员数量
LCIMConversation#memberCount
方法可以用来查询普通对话的成员总数,在聊天室中,它返回的就是实时在线的人数:消息等级
为了保证消息的时效性,当聊天室消息过多导致客户端连接堵塞时,服务器端会选择性地丢弃部分非高等级的消息。目前支持的消息等级有:
MessagePriority.HIGH
MessagePriority.NORMAL
MessagePriority.LOW
消息等级默认为
NORMAL
。消息等级在发送接口的参数中设置。以下代码演示了如何发送一个高等级的消息:
消息免打扰
假如某一用户不想再收到某对话的消息提醒,但又不想直接退出对话,可以使用静音操作,即开启「免打扰模式」。
比如 Tom 工作繁忙,对某个对话设置了静音:
设置静音之后,iOS 及启用混合推送的 Android 用户就不会收到推送消息了。与之对应的就是取消静音的操作(
Conversation#unmute
方法),即取消免打扰模式。消息内容的实时过滤
对于开放聊天室来说,内容的审核和实时过滤是产品运营上的一个基本要求。我们即时通讯服务默认提供了敏感词过滤的功能,多人的 普通对话、聊天室和系统对话里面的消息都会进行实时过滤。 命中的敏感词将会被替换为
***
。 消息内容实时过滤属于系统层面的修改消息,发送者会收到MESSAGE_UPDATE
事件。 应用可以在客户端监听该事件,实现相应的业务逻辑,相关代码示例可以参考《即时通讯开发指南》第二篇的《修改消息》一节。过滤的词库由即时通讯服务统一提供。商用版应用还支持开发者使用自定义敏感词词库,只需在 云服务控制台 > 即时通讯 > 设置 中上传敏感词文件。 敏感词文件为 UTF-8 编码的纯文本文件,一行一个敏感词。 开发者上传的自定义敏感词词库会替换默认提供的词库。
如果开发者有较为复杂的过滤需求,我们推荐使用云引擎 hook
_messageReceived
来实现过滤,在 hook 中开发者对消息的内容有完全的控制力。使用临时对话
临时对话是一个全新的概念,它解决的是一种特殊的聊天场景:
clientId
)临时对话最大的特点是 较短的有效期,这个特点可以解决对话的持久化存储在服务端占用的存储资源越来越大、开发者需要支付的成本越来越高的问题,也可以应对一些临时聊天的场景。诸如电商售前和售后在线聊天的客服系统,我们推荐使用临时对话。
临时对话实例
IMConversation
有专门的createTemporaryConversation
方法用于创建临时对话:与其他对话类型不同的是,临时对话有一个 重要 的属性:TTL。它标记着这个对话的有效期,系统默认是 1 天,但是在创建对话的时候是可以指定这个时间的,最高不超过 30 天。如果您的需求是一定要超过 30 天,请使用普通对话。传入 TTL 创建临时对话的代码如下:
临时对话的其他操作与普通对话无异。
进一步阅读