AV.Cloud.onIMConversationRemove(async (request) => {
const supporters = ["Bast", "Hypnos", "Kthanid"];
const members = request.params.members;
for (const member of members) {
if (supporters.includes(member)) {
return {
"reject": true,
"code": 1928,
"detail": `不允许移除官方运营人员 ${member}`,
};
}
}
return {};
}
@engine.define
def _conversationRemove(**params):
supporters = ["Bast", "Hypnos", "Kthanid"]
members = params["members"]
for member in members:
if member in supporters:
return {
"reject": True,
"code": 1928,
"detail": f"不允许移除官方运营人员 {member}"
}
return {}
AV.Cloud.onIMConversationUpdate((request) => {
if ('attr' in requst.params && 'name' in request.params.attr) {
return {
"reject": true,
"code": 1949,
"detail": "对话名称不可修改",
};
}
});
@engine.define
def _conversationUpdate(**params):
if ('attr' in params) and ('name' in params['attr']):
return {
"reject": True,
"code": 1949,
"detail": "对话名称不可修改"
}
四,详解消息 hook 与系统对话
本章导读
在前一篇安全与签名、黑名单和权限管理、玩转聊天室和临时对话中,我们解释了一些第三方鉴权以及成员权限设置方面的问题,在这里我们会更进一步,给大家说明:
万能的 Hook 机制
完全开放的架构,支持强大的业务扩展能力,是即时通讯服务的特色之一,这种优势的体现就是这里将要给大家介绍的「Hook 机制」。
Hook 与即时通讯服务的关系
Hook 也可以称为「钩子」,是一种特殊的消息处理机制,与 Windows 平台下的中断机制类似,允许应用方拦截并处理即时通讯过程中的多种事件和消息,从而达到实现自定义业务逻辑的目的。
以 _messageRecieved Hook 为例,它在消息送达服务器后会被调用,在 Hook 内可以捕获消息内容、消息发送者、消息接收者等信息,这些信息均能在 Hook 内做修改并将修改后的值转交回服务器,服务器会使用修改后的消息继续完成消息投递工作。最终收消息用户收到的会是被 Hook 修改过后的消息,而不再是最初送达服务器的原始消息。Hook 也可以选择拒绝消息发送,服务器会在给客户端回复消息被 Hook 拒绝后丢弃消息不再完成后续消息处理及转发流程。
需要注意的是,默认情况下如果 Hook 调用失败,例如超时、返回状态码非 200 的结果等,服务器会忽略 Hook 的错误继续处理原始请求。如果您需要改变这个行为,可以在云服务控制台 > 即时通讯 > 设置 > 即时通讯设置 内开启 「Hook 调用失败时返回错误给客户端并放弃继续处理请求」。开启后如果 Hook 调用失败,服务器会返回错误信息给客户端告知 Hook 调用错误,并拒绝继续处理请求。
消息类 Hook
一条消息,在即时通讯的流程中,从终端用户 A 发送开始,到其他用户接收到为止,考虑到存在接收方在线/不在线的可能,会经历多个不同阶段,这里每一个阶段都会触发 Hook 函数:
消息达到服务器,群组成员已解析完成之后,发送给收件人之前调用。开发者在这里还可以修改消息内容,实时改变消息接收者的列表,以及其他类似操作。
消息发送完成后调用。开发者在这里可以完成业务统计,或将消息中转备份到己方服务器,以及其他类似操作。
消息发送完成,存在离线的收件人,在发推送给收件人之前调用。开发者在这里可以动态修改离线推送的通知内容,或通知目的设备的列表,以及其他类似操作。
收到消息修改请求,发送修改后的消息给收件人之前调用。与新发消息一样,开发者在这里可以再次修改消息内容,实时改变消息接收者的列表,以及其他类似操作。
对话类 Hook
在对话创建和成员变动等更改性操作前后,都可以触发 Hook 函数,进行额外的处理:
创建对话,在签名校验(如果开启)之后,实际创建之前调用。开发者在这里可以为新的「对话」添加其他内部属性,或完成操作鉴权,以及其他类似操作。
创建对话完成后调用。开发者在这里可以完成业务统计,或将对话数据中转备份到己方服务器,以及其他类似操作。
向对话添加成员,在签名校验(如果开启)之后,实际加入之前调用,包括主动加入和被其他用户加入两种情况。开发者可以在这里根据内部权限设置批准或驳回这一请求,以及其他类似操作。
从对话中踢出成员,在签名校验(如果开启)之后,实际踢出之前调用,用户自己退出对话不会调用。开发者可以在这里根据内部权限设置批准或驳回这一请求,以及其他类似操作。
用户加入对话,在加入成功后调用。
用户离开对话,在离开成功后调用。
修改对话名称、自定义属性,设置或取消对话消息提醒,在实际修改之前调用。开发者在这里可以为新的「对话」添加其他内部属性,或完成操作鉴权,以及其他类似操作。
客户端上下线 Hook
在客户端上线和下线的时候,可以触发 Hook 函数:
客户端上线,客户端登录成功后调用。
客户端下线,客户端登出成功或意外下线后调用。
开发者可以利用这两个 Hook 函数,结合 LeanCache 来完成一组客户端实时状态查询的 endpoint,具体可以参考文档《即时通讯中的在线状态查询》。
Hook 与云引擎的关系
因为 Hook 发生在即时通讯的在线处理环节,而即时通讯服务端每秒钟需要处理的消息和对话事件数量远超大家的想象,出于性能考虑,我们要求开发者使用云引擎来实现 Hook 函数。
即时通讯的云引擎 Hook 要求云引擎部署在云引擎的 生产环境,测试环境仅用于开发者手动调用测试。由于缓存的原因,首次部署的云引擎 Hook 需要至多三分钟来正式生效,后续修改会实时生效。
Hook API 细节与使用场景详解
与
conversation
相关的 hook 可以在应用签名之外增加额外的权限判断,控制对话是否允许被建立、某些用户是否允许被加入对话等。你可以用这一 hook 实现黑名单功能。_messageReceived
这个 hook 发生在消息到达云端之后。如果是群组消息,我们会解析出所有消息收件人。
你可以通过返回参数控制消息是否需要被丢弃,删除个别收件人,还可以修改消息内容,例如过滤应用中的敏感词。返回空对象(
response.success({})
)则会执行系统默认的流程。请注意,在这个 hook 的代码实现的任何分支上 请确保最终会调用
response.success
返回结果,使得消息可以尽快投递给收件人。这个 hook 将 阻塞发送流程,因此请尽量减少无谓的代码调用,提升效率。如果你使用了默认提供的富媒体消息格式,云引擎参数中的
content
接收的是 JSON 结构的字符串形式。关于这个结构的详细说明,请参考《即时通讯 REST API 使用指南》的《富媒体消息格式说明》一节。参数:
fromPeer
convId
toPeers
clientId
。transient
bin
content
bin
为true
,则该字段为原始消息内容做 Base64 转码后的结果。receipt
timestamp
system
sourceIP
参数示例:
返回值:
drop
code
drop
为true
时可以下发一个应用自定义的整型错误码。detail
drop
为true
时可以下发一个应用自定义的错误说明字符串。bin
content
内是否为二进制消息,如果不提供则为请求中的bin
值。content
content
,如果不提供则保留原消息。如果bin
为true
,则content
需要是二进制消息内容做 Base64 转码后的结果。toPeers
示例代码:
而实际上启用上述代码之后,一条消息的时序图如下:
_receiversOffline
这个 hook 发生在有收件人离线的情况下,你可以通过它来自定义离线推送行为,包括推送内容、被推送用户或略过推送。你也可以直接在 hook 中触发自定义的推送。发往聊天室的消息不会触发此 hook。
参数:
fromPeer
convId
offlinePeers
content
timestamp
mentionAll
mentionOfflinePeers
mentionAll
为true
,则该参数为空,表示所有offlinePeers
参数内的成员全部被 @。返回值:
skip
offlinePeers
pushMessage
force
offlinePeers
里mute
的用户,默认false
。示例代码:
_messageSent
在消息发送完成后执行,对消息发送性能没有影响,可以用来执行相对耗时的逻辑。
参数:
fromPeer
convId
msgId
onlinePeers
offlinePeers
transient
system
bin
content
receipt
timestamp
sourceIP
参数示例:
返回值:
这个 hook 不会对返回值进行检查。只需返回
{}
即可。示例代码:
下面代码演示了日志记录相关的操作(在消息发送完后,在云引擎中打印一下日志):
_messageUpdate
这个 hook 发生在修改消息请求到达云端,云端正式修改消息之前。
你可以通过返回参数控制修改消息请求是否需要被丢弃,删除个别收件人,或再次修改这个修改消息请求中的消息内容。
请注意,在这个 hook 的代码实现的任何分支上 请确保最终会调用
response.success
返回结果,使得修改消息可以尽快投递给收件人。这个 hook 将 阻塞发送流程,因此请尽量减少无谓的代码调用,提升效率。如果你使用了默认提供的富媒体消息格式,云引擎参数中的
content
接收的是 JSON 结构的字符串形式。关于这个结构的详细说明,请参考《即时通讯 REST API 使用指南》的《富媒体消息》一节。参数:
fromPeer
convId
toPeers
clientId
。bin
content
bin
为true
,则该字段为原始消息内容做 Base64 转码后的结果。timestamp
msgId
sourceIP
recall
system
返回值:
drop
code
drop
为true
时可以下发一个应用自定义的整型错误码。detail
drop
为true
时可以下发一个应用自定义的错误说明字符串。bin
content
内是否为二进制消息,如果不提供则为请求中的bin
值。content
content
,如果不提供则保留原消息。如果bin
为true
,则content
需要是二进制消息内容做 Base64 转码后的结果。toPeers
_conversationStart
在创建对话时调用,发生在签名验证(如果开启)之后、创建对话之前。
参数:
initBy
clientId
。members
attr
参数示例:
返回值:
reject
false
。code
reject
为true
时可以下发一个应用自定义的整型错误码。detail
reject
为true
时可以下发一个应用自定义的错误说明字符串。例如,初始成员不足四人,不允许创建对话:
_conversationStarted
对话创建后调用。
参数:
convId
返回值:
这个 hook 不对返回值进行处理,只需返回
{}
即可。例如,创建对话后,将对话的 ID 存储到 LeanCache 的最近创建对话列表:
_conversationAdd
在将用户加入到对话时调用,发生在签名验证(如果开启)之后、加入对话之前,包括主动加入和被其他用户加入两种情况都会触发。注意如果在创建对话时传入了其他用户的
clientId
作为成员,则不会触发该 hook。如果是自己加入,那么initBy
和members
的唯一元素是一样的。参数:
initBy
clientId
。members
convId
返回值:
reject
false
。code
reject
为true
时可以下发一个应用自定义的整型错误码。detail
reject
为true
时可以下发一个应用自定义的错误说明字符串。例如,不允许某成员创建的对话新增成员:
_conversationRemove
从对话中移除成员,在签名校验(如果开启)之后、实际踢出之前触发,用户自己退出对话不会触发。
参数:
initBy
members
convId
返回值:
reject
false
。code
reject
为true
时可以下发一个应用自定义的整型错误码。detail
reject
为true
时可以下发一个应用自定义的错误说明字符串。例如,某个应用会让官方运营人员加入每个聊天群,希望加上群主无法踢掉官方运营的限制:
_conversationAdded
成员成功加入对话后调用。
参数:
initBy
convId
members
返回值:
这个 hook 不会对返回值进行检查。
例如,如果一个群一次性加入了 10 个以上的成员,那么给运营人员发送一条通知短信:
_conversationRemoved
成员成功离开对话后调用。
参数:
initBy
convId
members
返回值:
这个 hook 不会对返回值进行检查。
例如,如果用户自行离开了对话,那么将这个对话的 ID 存储到 LeanCache(应用可以利用这些数据实现展示「最近离开的对话」乃至重新加入的功能):
_conversationUpdate
在修改对话名称、自定义属性,设置或取消对话消息提醒之前调用。
参数:
initBy
convId
mute
attr
mute
和attr
参数互斥,不会同时传递。返回值:
reject
false
。code
reject
为true
时可以下发一个应用自定义的整型错误码。detail
reject
为true
时可以下发一个应用自定义的错误说明字符串。attr
mute
mute
和attr
参数互斥,不能同时返回。并且返回值必须与请求对应,请求中如果带着attr
,则返回值中只有attr
参数有效,返回mute
会被丢弃。同理,请求中如果带着mute
,返回值中如果有attr
则attr
会被丢弃。例如,不允许修改对话名称:
_clientOnline
客户端上线,客户端登录成功后调用。
请注意本 Hook 仅作为用户上线后的通知。如果用户快速的进行上下线切换或有多个设备同时进行上下线,则上线和下线 Hook 调用顺序并不做严格保证,即可能出现某用户
_clientOffline
Hook 先于_clientOnline
Hook 而调用。参数:
返回:
这个 hook 不会对返回值进行检查。
例如,客户端上线后更新 LeanCache,供查询客户端的实时在线状态:
_clientOffline
在客户端登出成功或意外下线后调用。
请注意本 Hook 仅作为用户下线后的通知。如果用户快速的进行上下线切换或有多个设备同时进行上下线,则上线和下线 Hook 调用顺序并不做严格保证,即可能出现某用户
_clientOffline
Hook 先于_clientOnline
Hook 而调用。参数:
tag
冲突被踢下线,4 代表用户被 API 踢下线default
表示主动登录时不会根据 tag 踢其它设备下线,其它情况下会踢当前登录的相同 tag 的设备下线可能出现的错误信息说明:
返回:
这个 hook 不会对返回值进行检查。
例如,客户端下线后更新 LeanCache,供查询客户端的实时在线状态:
《即时通讯中的在线状态查询》提供了完整的 Node.js 示例(包括 LeanCache 连接,久未上线的客户端清理,配套的返回在线状态的云函数,以及如何在客户端调用),可以参考。
「系统对话」的使用
系统对话可以用于实现机器人自动回复、公众号、服务账号等功能。在我们的 官方聊天 Demo 中就有一个使用系统对话 hook 实现的机器人 MathBot,它能计算用户发送来的数学表达式并返回结果,其服务端源码 可以从 GitHub 上获取。
系统对话的创建
系统对话也是对话的一种,创建后也是在
_Conversation
表中增加一条记录,只是该记录sys
列的值为true
,从而与普通会话进行区别。具体创建方法请参考《即时通讯 REST API 使用指南》的《创建服务号》一节。系统对话消息的发送
系统对话给用户发消息请参考《即时通讯 REST API 使用指南》的《给任意用户单独发消息》一节。用户给系统对话发送消息跟用户给普通对话发消息方法一致。
您还可以利用系统对话发送广播消息给全部用户。相比遍历所有用户 ID 逐个发送,广播消息只需要调用一次 REST API。广播消息具有以下特征:
除此以外广播消息与普通消息的处理完全一致。广播消息的发送可以参考《即时通讯 REST API 使用指南》的《全局广播》一节。
获取系统对话消息记录
获取系统对话给用户发送的消息记录请参考:《即时通讯 REST API 使用指南》的《查询服务号给某用户发的消息》一节。
获取用户给系统对话发送的消息记录有以下两种方式实现:
_SysMessage
表方式,在应用首次有用户发送消息给某系统对话时自动创建,创建后我们将所有由用户发送到系统对话的消息都存储在该表中。系统对话消息结构
_SysMessage
存储用户发给系统对话的消息,各字段含义如下:
ackAt
bin
conv
Pointer
。data
from
clientId
。fromIp
msgId
timestamp
Web Hook
需要开发者自行在 云服务控制台 > 即时通讯 > 设置 > 服务号消息回调 定义,来实时接收用户发给系统对话的消息,消息的数据结构与上文所述的
_SysMessage
一致。当有用户向系统对话发送消息时,我们会通过 HTTP POST 请求将 JSON 格式的数据发送到用户设置的 Web Hook 上。请注意,我们调用 Web Hook 时并不是一次调用只发送一条消息,而是会以批量的形式将消息发送过去。从下面的发送消息格式中能看到,JSON 的最外层是个
Array
。超时时间为 5 秒,当用户 hook 地址超时没有响应,我们会重试至多 3 次。
发送的消息格式为:
即时通讯开发指南一览