Skip to main content
Version: v3

4. Hooks and System Conversations

Introduction

In the previous chapter, Security, Permission Management, Chat Rooms, and Temporary Conversations, we introduced our third-party signing mechanism as well as how you can allow owners and managers of a conversation to edit other members’ permissions. In this chapter, we will cover the following functionalities offered by the Instant Messaging service:

  • Hooks
  • System conversations

Hooks

Instant Messaging is built with an open architecture that has strong extensibility, allowing you to do more than implement basic chatting features. Instant Messaging provides a collection of hooks that make it handy for you to utilize such extensibility. We’ll delve into them later in this section.

The Connection Between Hooks and Instant Messaging

Hooks are a special message-handling mechanism for your app to intercept and process the various types of events and messages sent through it. They allow you to trigger custom logics when these events and messages are sent, enabling you to extend the existing features provided by the Instant Messaging service.

Take _messageRecieved as an example. This hook gets triggered when a message arrives at the server. Within the hook, you can obtain the properties of the message including its content, sender, and receivers. All these properties can be modified within the hook before they get taken over by the server. The server will then complete the delivery of the message with the modified properties and the message seen by the receivers will be the one with the modified properties instead of the original message. The hook can also reject the message so that the message won’t be seen by the receivers anymore.

Keep in mind that by default, if a hook fails due to timing out or returning a non-200 status code, the server will disregard the failure and continue processing the original request. You can change this behavior by enabling Return error and stop processing request when hook failed under Developer Center > Your game > Game Services > Cloud Services > Instant Messaging > Settings > Instant Messaging settings. With this option enabled, if a hook fails, the server will return the error message to the client and abort the request.

Hooks for Messages

After being sent out from the sender and before being received by the receivers, a message would go through a series of stages determined by the online statuses of the receivers. You can set up a hook that gets triggered for each of the stages:

  • _messageReceived
    This hook gets triggered after the server receives a message and parses the members in the group, but before the message gets delivered to the receivers. Here you can perform operations like modifying the message’s content and receivers.
  • _messageSent
    This hook gets triggered after a message gets delivered. Here you can perform operations like logging and making a copy of the message on your backup server.
  • _receiversOffline
    This hook gets triggered after a message gets delivered with some of the receivers offline, but before push notifications get sent to the offline receivers. Here you can perform operations like dynamically updating the content and device list of the push notifications.
  • _messageUpdate
    This hook gets triggered after the server receives a request for updating a message, but before the updated message gets delivered to the receivers. Similar to the situation when a new message is sent, here you can perform operations like modifying the message’s content and receivers.

Hooks for Conversations

A hook can be triggered before or after a conversation-related operation takes place, like when a conversation gets created or when the member list of a conversation gets updated:

  • _conversationStart
    When a conversation is being created, this hook gets triggered after the signature validation (if enabled) has been completed but before the conversation actually gets created. Here you can perform operations like adding additional internal attributes to the conversation and performing authentication.
  • _conversationStarted
    This hook gets triggered after a conversation gets created. Here you can perform operations like logging and making a copy of the conversation on your backup server.
  • _conversationAdd
    When a member is joining or being added to a conversation, this hook gets triggered after the signature validation (if enabled) has been completed but before the member actually joins or gets added to the conversation. Here you can perform operations like determining whether the request shall be accepted or declined.
  • _conversationRemove
    When a member is being removed from a conversation, this hook gets triggered after the signature validation (if enabled) has been completed but before the member actually gets removed from the conversation. This hook doesn’t get triggered when a member is leaving a conversation. Here you can perform operations like determining whether the request shall be accepted or declined.
  • _conversationAdded
    This hook gets triggered after a user successfully joins a conversation.
  • _conversationRemoved
    This hook gets triggered after a user successfully leaves a conversation.
  • _conversationUpdate
    When a conversation’s name, custom attributes, or notification settings are being updated, this hook gets triggered before the update actually takes place. Here you can perform operations like adding additional internal attributes to the conversation and performing authentication.

Hooks for Client Status Changes

A hook can be triggered when a client logs in or logs out:

  • _clientOnline
    This hook gets triggered when a client logs in successfully.
  • _clientOffline
    This hook gets triggered when a client logs out successfully or loses connection unexpectedly.

You can use these hooks together with LeanCache to implement an endpoint for looking up the online statuses of clients.

Hooks and Cloud Engine

To maintain the necessary performance for handling an abundance of messages, the Instant Messaging service itself doesn’t provide the computing resources for running hooks. In order to use hooks, you will have to set up Cloud Engine instances for your application and deploy hooks onto these instances.

The hooks for Instant Messaging will only take effect when deployed to the production environment of Cloud Engine. The staging environment shall be used for testing hooks but the hooks deployed there can only be triggered manually. Due to the existence of the cache, it may take up to 3 minutes for hooks to take effect if you’re deploying hooks to Cloud Engine for the first time. After that, the hooks deployed will take effect immediately.

Hooks API

Conversation-related hooks can be used to perform additional permission checks besides those taken care of by the signing mechanism, controlling whether a conversation can be created or whether a user can be allowed into a conversation. One thing you can do with this hook is to implement a blocklist for your application.

_messageReceived

This hook gets triggered after a message arrives at the server. If the message is sent to a group, the server will parse all the receivers of the message.

You can have the hook return a value to control whether the message should be discarded, which receivers should be removed, and what the updated message should be if the message is to be updated. If the hook returns an empty object (response.success({})), the message will go through the default workflow.

If the hook contains conditionals, please be careful to make sure the hook will always invoke response.success in the end to return a result so that the message can be delivered without delay. The hook will block the process of message delivery, which means that you should keep the hook efficient by eliminating unnecessary invocations within the hook.

For a rich media message, the content parameter will be a string containing a JSON object. See Instant Messaging REST API Guide for more information about the structure of this object.

Parameters:

ParameterDescription
fromPeerThe ID of the sender.
convIdThe ID of the conversation the message belongs to.
toPeersThe clientIds of the members in the conversation.
transientWhether this is a transient message.
binWhether the content of the original message is binary.
contentThe string representing the content of the message. If bin is true, this string will be the original message encoded in Base64 format.
receiptWhether a receipt is requested.
timestampThe timestamp the server received the message (in milliseconds).
systemWhether the message belongs to a system conversation.
sourceIPThe IP address of the sender.

Example arguments:

{
"fromPeer": "Tom",
"receipt": false,
"groupId": null,
"system": null,
"content": "{\"_lctext\":\"Holy crap!\",\"_lctype\":-1}",
"convId": "5789a33a1b8694ad267d8040",
"toPeers": ["Jerry"],
"bin": false,
"transient": false,
"sourceIP": "121.239.62.103",
"timestamp": 1472200796764
}

Return values:

ParameterConstraintDescription
dropOptionalThe message will be discarded if this value is true.
codeOptionalA custom error code (integer) to be returned when drop is true.
detailOptionalA custom error message (string) to be returned when drop is true.
binOptionalWhether the returned content is binary. If omitted, this will be the same as the value of bin in the request.
contentOptionalThe updated content. If omitted, the original message content will be used. If bin is true, this should be the message encoded in Base64 format.
toPeersOptionalAn array containing the updated receivers. If omitted, the original receivers will be used.

Code example:

AV.Cloud.onIMMessageReceived((request) => {
let content = request.params.content;
let processedContent = content.replace("crap", "**");
// Must provide a return value, or an error will occur
return {
content: processedContent,
};
});

With the code above enabled, the sequence diagram of a message will be:

sequenceDiagram SDK->>RTM: 1. Send a message RTM-->>Engine: 2. Trigger the _messageReceived hook Engine-->>RTM: 3. Return the result of the hook RTM-->>SDK: 4. Send the result of the hook to the receiver
  • The diagram above assumes that all the members in the conversation are online. The sequence will be slightly different if some of the members are offline. We will talk about this in the next section.
  • RTM refers to the cluster for the Instant Messaging service and Engine refers to that for the Cloud Engine service. These two clusters communicate through our internal network.

_receiversOffline

This hook gets triggered when some of the receivers are offline. A common use case of this hook is to customize the content and receivers of push notifications. You can even trigger custom push notifications with this hook. Keep in mind that messages sent to chat rooms won’t trigger this hook.

Parameters:

ParameterDescription
fromPeerThe ID of the sender.
convIdThe ID of the conversation the message belongs to.
offlinePeersAn array containing the receivers that are offline.
contentThe content of the message.
timestampThe timestamp the server received the message (in milliseconds).
mentionAllA boolean indicating whether all the members are mentioned.
mentionOfflinePeersMembers who are offline but got mentioned by this message. If mentionAll is true, this parameter will be empty, indicating that all the members in offlinePeers are mentioned.

Return values:

ParameterConstraintDescription
skipOptionalIf set to true, push notifications will be skipped. This could be useful if you have already triggered push notifications in a different manner.
offlinePeersOptionalAn array containing the updated receivers.
pushMessageOptionalThe content of the push notifications. You can provide a JSON object with a custom structure.
forceOptionalIf set to true, push notifications will be sent to the users in offlinePeers who muted the conversation. Defaults to false.

Code example:

AV.Cloud.onIMReceiversOffline((request) => {
let params = request.params;
let content = params.content;

// params.content is the content of the message
let shortContent = content;

if (shortContent.length > 6) {
shortContent = content.slice(0, 6);
}

console.log("shortContent", shortContent);

return {
pushMessage: JSON.stringify({
// Increment the number of unread messages; you can provide a number as well
badge: "Increment",
sound: "default",
// Use the dev certificate
_profile: "dev",
alert: shortContent,
}),
};
});

_messageSent

This hook gets triggered after a message gets delivered. It won’t impact the performance of the message-delivery process, so you can leave time-consuming operations here.

Parameters:

ParameterDescription
fromPeerThe ID of the sender.
convIdThe ID of the conversation the message belongs to.
msgIdThe ID of the message.
onlinePeersThe list of the online users’ IDs.
offlinePeersThe list of the offline users’ IDs.
transientWhether this is a transient message.
systemWhether the message belongs to a system conversation.
binWhether this is a binary message.
contentThe string representing the content of the message.
receiptWhether a receipt is requested.
timestampThe timestamp the server received the message (in milliseconds).
sourceIPThe IP address of the sender.

Example arguments:

{
"fromPeer": "Tom",
"receipt": false,
"onlinePeers": [],
"content": "12345678",
"convId": "5789a33a1b8694ad267d8040",
"msgId": "fptKnuYYQMGdiSt_Zs7zDA",
"bin": false,
"transient": false,
"sourceIP": "114.219.127.186",
"offlinePeers": ["Jerry"],
"timestamp": 1472703266522
}

Return values:

The return value of this hook won’t be checked. You can just have the hook return {}.

Code example:

The code below shows how you can have a log printed to Cloud Engine when a message gets delivered:

AV.Cloud.onIMMessageSent((request) => {
console.log("params", request.params);
});

_messageUpdate

This hook gets triggered after the server receives a request for updating a message, but before the updated message gets delivered to the receivers.

You can have the hook return a value to control whether the request for updating the message should be discarded, which receivers should be removed, and what the updated message should be if the message is to be updated again.

If the hook contains conditionals, please be careful to make sure the hook will always invoke response.success in the end to return a result so that the updated message can be delivered without delay. The hook will block the process of message delivery, which means that you should keep the hook efficient by eliminating unnecessary invocations within the hook.

For a rich media message, the content parameter will be a string containing a JSON object. See Instant Messaging REST API Guide for more information about the structure of this object.

Parameters:

ParameterDescription
fromPeerThe ID of the sender.
convIdThe ID of the conversation the message belongs to.
toPeersThe clientIds of the members in the conversation.
binWhether the content of the original message is binary.
contentThe string representing the content of the message. If bin is true, this string will be the original message encoded in Base64 format.
timestampThe timestamp the server received the message (in milliseconds).
msgIdThe ID of the message being updated.
sourceIPThe IP address of the sender.
recallWhether the message is recalled.
systemWhether the message belongs to a system conversation.

Return values:

ParameterConstraintDescription
dropOptionalThe request for updating the message will be discarded if this value is true.
codeOptionalA custom error code (integer) to be returned when drop is true.
detailOptionalA custom error message (string) to be returned when drop is true.
binOptionalWhether the returned content is binary. If omitted, this will be the same as the value of bin in the request.
contentOptionalThe updated content. If omitted, the original message content will be used. If bin is true, this should be the message encoded in Base64 format.
toPeersOptionalAn array containing the updated receivers. If omitted, the original receivers will be used.

_conversationStart

When a conversation is being created, this hook gets triggered after the signature validation (if enabled) has been completed but before the conversation actually gets created.

Parameters:

ParameterDescription
initByThe clientId of the initiator of the conversation.
membersAn array containing the initial members of the conversation.
attrAdditional attributes assigned to the conversation.

Example arguments:

{
"initBy": "Tom",
"members": ["Tom", "Jerry"],
"attr": {
"name": "Tom & Jerry"
}
}

Return values:

ParameterConstraintDescription
rejectOptionalWhether to reject the request. Defaults to false.
codeOptionalA custom error code (integer) to be returned when reject is true.
detailOptionalA custom error message (string) to be returned when reject is true.

For example, to refuse a conversation to be created if it contains less than 4 initial members:

AV.Cloud.onIMConversationStart((request) => {
if (request.params.members.length < 4) {
return {
reject: true,
code: 1234,
detail: "Please invite at least 3 people to the conversation",
};
} else {
return {};
}
});

_conversationStarted

This hook gets triggered after a conversation gets created.

Parameters:

ParameterDescription
convIdThe ID of the conversation being created.

Return values:

The return value of this hook won’t be checked. You can just have the hook return {}.

For example, to save the ID of the conversation to a list of recently created conversations on LeanCache after a conversation gets created:

AV.Cloud.onIMConversationStarted((request) => {
redisClient.lpush("recent_conversations", request.params.convId);
return {};
});

_conversationAdd

When a member is joining or being added to a conversation, this hook gets triggered after the signature validation (if enabled) has been completed but before the member actually joins or gets added to the conversation. Keep in mind that this hook won’t be triggered in the situation when a conversation is being created with other users’ clientIds as members. If a member is joining a conversation, initBywill be the same as the only element ofmembers`.

Parameters:

ParameterDescription
initByThe clientId of the initiator.
membersAn array containing the members joining the conversation.
convIdThe ID of the conversation.

Return values:

ParameterConstraintDescription
rejectOptionalWhether to reject the request. Defaults to false.
codeOptionalA custom error code (integer) to be returned when reject is true.
detailOptionalA custom error message (string) to be returned when reject is true.

For example, to refuse new members to be added to the conversation created by a specific member:

AV.Cloud.onIMConversationAdd((request) => {
if (request.params.initBy === "Tom") {
return {
reject: true,
code: 9890,
detail: "This is a private conversation. You cannot add anyone else to it.",
};
} else {
return {};
}
});

_conversationRemove

When a member is being removed from a conversation, this hook gets triggered after the signature validation (if enabled) has been completed but before the member actually gets removed from the conversation. This hook doesn’t get triggered when a member is leaving a conversation.

Parameters:

ParameterDescription
initByThe initiator of the operation.
membersAn array containing the members to be removed.
convIdThe ID of the conversation.

Return values:

ParameterConstraintDescription
rejectOptionalWhether to reject the request. Defaults to false.
codeOptionalA custom error code (integer) to be returned when reject is true.
detailOptionalA custom error message (string) to be returned when reject is true.

For example, to have some staff members in each of the conversations of an application that cannot be removed even by the owner of the conversation:

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": `You cannot remove the staff member ${member}`,
};
}
}
return {};
}

_conversationAdded

This hook gets triggered after a user successfully joins a conversation.

Parameters:

ParameterDescription
initByThe initiator of the operation.
convIdThe ID of the conversation.
membersAn array containing the IDs of the new members.

Return values:

The return value of this hook won’t be checked.

For example, to send a text message to a staff member if more than 10 members are added to a conversation at once:

AV.Cloud.onIMConversationAdded((request) => {
if (request.params.members.length > 10) {
AV.Cloud.requestSmsCode({
mobilePhoneNumber: "+15559463664",
template: "Group_Notice",
sign: "sign_example",
conv_id: request.params.convId,
}).then(
function () {
/* Succeeded */
},
function (err) {
/* Failed */
}
);
}
});

_conversationRemoved

This hook gets triggered after a user successfully leaves a conversation.

Parameters:

ParameterDescription
initByThe initiator of the operation.
convIdThe ID of the conversation.
membersAn array of the IDs of the users removed from the conversation.

Return values:

The return value of this hook won’t be checked.

For example, to save the ID of the conversation to a list of recently left conversations on LeanCache after a user leaves a conversation:

AV.Cloud.onIMConversationRemoved((request) => {
const initBy = request.params.initBy;
const members = request.params.members;
if (members.length === 1) {
if (members[0] === initBy) {
redisClient.lpush(initBy, request.params.convId);
}
}
});

_conversationUpdate

When a conversation’s name, custom attributes, or notification settings are being updated, this hook gets triggered before the update actually takes place.

Parameters:

ParameterDescription
initByThe initiator of the operation.
convIdThe ID of the conversation.
muteWhether to disable notifications for the current conversation.
attrAttributes to be set to the conversation.

mute and attr are mutually exclusive and won’t show up together.

Return values:

ParameterConstraintDescription
rejectOptionalWhether to reject the request. Defaults to false.
codeOptionalA custom error code (integer) to be returned when reject is true.
detailOptionalA custom error message (string) to be returned when reject is true.
attrOptionalThe updated attributes to be set to the conversation. If omitted, the attr in the request will be used.
muteOptionalThe updated setting for disabling notifications. If omitted, the mute in the request will be used.

mute and attr are mutually exclusive and can’t be returned together. The return value should also match what’s in the request. If the request contains attr, only the attr in the return value will take effect. If the request contains mute, the attr in the return value will be discarded if it exists.

For example, to prevent the names of conversations from being updated:

AV.Cloud.onIMConversationUpdate((request) => {
if ("attr" in request.params && "name" in request.params.attr) {
return {
reject: true,
code: 1949,
detail: "The name of the conversation cannot be updated.",
};
}
});

_clientOnline

This hook gets triggered when a client logs in successfully.

Keep in mind that this hook only serves as a notification indicating that a user has gone online. If a user quickly goes online and offline (maybe with multiple devices), the sequence the _clientOnline and _clientOffline hooks get triggered can’t be guaranteed. This means that the _clientOffline hook may be triggered for a user before the _clientOnline hook gets triggered.

Parameters:

ParameterDescription
peerIdThe ID of the client logging in.
sourceIPThe IP address of the client logging in.
tagIf left empty or set to "default", other devices with the same tag won’t be logged out. Otherwise, other devices with the same tag will be logged out.
reconnectWhether to automatically reconnect for this log-in attempt. If left empty or set to 0, auto-reconnect will be disabled. If set to 1, auto-reconnect will be enabled.

Return values:

The return value of this hook won’t be checked.

For example, to update the data stored in LeanCache for looking up the online statuses of users:

AV.Cloud.onIMClientOnline((request) => {
// 1 means online
redisClient.set(request.params.peerId, 1);
});

_clientOffline

This hook gets triggered when a client logs out successfully or loses connection unexpectedly.

Keep in mind that this hook only serves as a notification indicating that a user has gone online. If a user quickly goes online and offline (maybe with multiple devices), the sequence the _clientOnline and _clientOffline hooks get triggered can’t be guaranteed. This means that the _clientOffline hook may be triggered for a user before the _clientOnline hook gets triggered.

Parameters:

ParameterDescription
peerIdThe ID of the client getting offline.
closeCodeHow the client got offline. 1 means the client logged out proactively. 2 means the connection was lost. 3 means the client was logged out due to a duplicate tag. 4 means the client was logged out by a request sent to the API.
closeMsgA message describing how the client got offline.
sourceIPThe IP address of the client closing the session. Will be omitted if the hook is triggered by a loss of connection.
tagProvided when the session got created. If left empty or set to "default", other devices with the same tag won’t be logged out. Otherwise, other devices with the same tag will be logged out.
errorCodeThe code of the error causing the loss of the connection; optional.
errorMsgThe message of the error causing the loss of the connection; optional.

Possible errors:

Error codeError messageDescription
4107READ_TIMEOUTThe connection timed out due to a lack of new messages or heartbeats for a while.
4108LOGIN_TIMEOUTThe connection timed out due to not logging in for a while.
4109FRAME_TOO_LONGThe WebSocket frame is too long.
4114UNPARSEABLE_RAW_MSGThe message is malformatted and cannot be parsed.
4200INTERNAL_ERRORThere is an internal error in our server.

Return values:

The return value of this hook won’t be checked.

For example, to update the data stored in LeanCache for looking up the online statuses of users:

AV.Cloud.onIMClientOffline((request) => {
// 0 means offline
redisClient.set(request.params.peerId, 0);
});

System Conversations

With system conversations, you can easily add functions like auto-reply, official accounts, and service accounts to your application. We have a demo that contains a MathBot that can calculate the mathematical expressions sent from users and respond with the results, which is implemented using hooks for system conversations. You can find the server-side program of this bot on GitHub.

Creating System Conversations

A system conversation is also a kind of conversation. When a system conversation is created, an entry will be added to the _Conversation table with sys being true. See Instant Messaging REST API Guide for more information on how to create a system conversation.

Sending Messages to System Conversations

Instant Messaging REST API Guide contains detailed instructions on how to send messages to users through system conversations. Besides this, users can send messages to system conversations in the same way they send messages to basic conversations.

System conversations can also be used to send broadcast messages to all the users of your application so you don’t have to manually obtain the IDs of these users before sending messages. All you need to do is to invoke the REST API for sending broadcast messages. A broadcast message has the following traits:

  • A broadcast message has to be associated with a conversation. The broadcast message will show up together with other messages in the history of the system conversation.
  • When a user gets online, they will be notified of any broadcast messages sent to them when they are offline.
  • You can set a TTL for a broadcast message so that users won’t be notified of it when getting online if the message has expired. Users will still be able to find the message in the history.
  • When a new user logs in for the first time, they will receive the last broadcast message that’s not expired.

Besides what’s mentioned above, a broadcast message will be treated in the same way as a basic message. See Instant Messaging REST API Guide for more information on how to send broadcast messages.

Getting History Messages of System Conversations

To obtain the messages sent to users through system conversations, see Instant Messaging REST API Guide.

To obtain the messages sent from users to system conversations, you can use one of the following ways:

  • Look up the _SysMessage table. This table gets created the first time a user of your application sends a message to a system conversation. All the messages sent from users to system conversations will be stored in this table.
  • Set up a Web Hook. You will have to define your Web Hook for receiving messages sent from users to system conversations.

Messages in System Conversations

_SysMessage

This table contains the messages sent from users to system conversations. It contains the following attributes:

AttributeDescription
ackAtThe time the message got delivered.
binWhether this is a binary message.
convA Pointer to the associated system conversation.
dataThe content of the message.
fromThe clientId of the sender.
fromIpThe IP address of the sender.
msgIdAn internal ID for the message.
timestampThe time the message got created.

Web Hook

To set up a Web Hook for receiving the messages sent from users to system conversations, go to Developer Center > Your game > Game Services > Cloud Services > Instant Messaging > Settings > Service conversation callback. The data structure of the messages conforms to the schema of the _SysMessage table mentioned earlier.

When users send messages to system conversations, our system will send an HTTP POST request containing data in JSON to the Web Hook you provided. Keep in mind that our system won’t generate a request for each message, but will combine multiple messages into a single request. You’ll notice that the outermost layer of the JSON in a request is an Array from the example below.

The timeout for each request will be 5 seconds. If your hook doesn’t respond to a request within the time limit, our system will retry up to 3 times.

The format of a request will look like this:

[
{
"fromIp": "121.238.214.92",
"conv": {
"__type": "Pointer",
"className": "_Conversation",
"objectId": "55b99ad700b0387b8a3d7bf0"
},
"msgId": "nYH9iBSBS_uogCEgvZwE7Q",
"from": "A",
"bin": false,
"data": "Hello sys",
"createdAt": {
"__type": "Date",
"iso": "2015-07-30T14:37:42.584Z"
},
"updatedAt": {
"__type": "Date",
"iso": "2015-07-30T14:37:42.584Z"
}
}
]

Previous Chapters