# 实时聊天

**URL:** https://heroiclabs.com/docs/zh/nakama/concepts/chat/
**Summary:** 通过直接、群组或公共聊天频道实现社区互动。允许用户浏览和加入所需要的聊天，或创建新的聊天。这些频道中的消息可以持续存在，允许离线用户稍后查看，也可以仅对在线用户可见。
**Keywords:** real-time chat, chat rooms, group chat, direct chat, private chat, persistence, message history, send chat message, leave chat
**Categories:** chat, real-time

---


# 实时聊天

实时聊天可以轻松活跃社区。

用户可以一对一聊天、群组聊天或在聊天室中聊天。消息可以包含图片、链接和其他内容。如果接收人在线，这些消息会立即传递给客户端，如果接收人离线，这些消息会存储在消息历史记录中，便于离线用户在连接时查看。

流经实时聊天引擎的每条消息都属于一个频道，内部使用这个频道识别哪些用户应该接收这些消息。用户在连接时明确要加入还是要离开频道。这样，用户可以轻松选择接收所关心的消息或者在繁忙时决定将某些频道“静音”。用户还可以同时加入多个频道，在多个群组或聊天室同时聊天。

## 聊天频道

有3种类型的频道：

1. 聊天室很适合公开聊天。任何用户都可以加入并参与，而无需获得许可。这些聊天室可以扩大容量，容纳数百万用户同时通信。这最适合用于现场参与类应用程序或有现场活动或锦标赛的游戏。

2. 群组聊天是私密的，仅对[群组](../groups/)中的用户开放。每位用户必须是群组成员，其他用户不能参与。您可以在小组玩法或协作中使用群组聊天。

3. 直接聊天是两个用户之间的私密聊天。每位用户在被邀请聊天时都会收到[通知](../notifications/)。两个用户都必须加入才能互相发送消息，防止恶意用户发送垃圾邮件。

### 持续性

默认所有频道都持续存在，通过这些频道发送的消息会被保存到数据库中，以后可在消息历史中查看。离线用户可以在下次连接时通过历史记录查看错过的消息。

如果消息仅应发送给在线用户，而不应保存在消息历史记录中，则客户端可以在[加入频道](#join-chat)时禁用维持。

### 隐藏的频道成员

默认加入频道的所有用户都对其他用户可见。当用户连接和断开连接时，现有的频道参与者将收到一个事件，而新加入频道的用户将收到频道中的现有用户的列表。

用户可以在连接时选择隐藏频道状态，这样他们就不会生成加入/退出通知，也不会出现在频道成员列表中。但这些用户仍然能够正常发送和接收实时消息。

## 加入聊天

要向其他用户发送消息，用户必须加入他们想要在其中交流的聊天频道。这也将使用户能够[实时接收到消息](#receive-messages)。

每位用户都可以在会话中加入许多聊天室、群组和直接聊天。同一用户也可以使用其他设备连接到同一个聊天，因为每个设备都被标识为单独的会话。

### 房间

用户可以创建动态房间进行聊天。房间有名称，当任何用户加入房间时，会在服务器上设置房间名称。可供加入的房间名称列表可以存储在客户端代码内，或通过远程配置的[存储记录](../storage/collections/)来存储。

{{< missing type="client" lang="shell" />}}
{{< missing type="client" lang="bash" />}}

{{< code type="client" lang="lua" framework="defold" >}}
```lua
local roomname = "MarvelMovieFans"
local persistence = true
local hidden = false

local result = socket.channel_join(roomname, socket.CHANNELTYPE_ROOM, persistence, hidden)

print("Now connected to channel id: " .. result.channel.id);
```
{{< / code >}}

{{< code type="client" >}}
```javascript
const roomname = "MarvelMovieFans";
const persistence = true;
const hidden = false;

// 1 = Room, 2 = Direct Message, 3 = Group
const result = await socket.joinChat(1, roomname, persistence, hidden);

console.log("Now connected to channel id: '%o'", result.channel.id);
```
{{< / code >}}

{{< code type="client" >}}
```csharp
var roomname = "MarvelMovieFans";
var persistence = true;
var hidden = false;
var channel = await socket.JoinChatAsync(roomname, ChannelType.Room, persistence, hidden);
Console.WriteLine("Now connected to channel id: '{0}'", channel.Id);
```
{{< / code >}}

{{< code type="client" >}}
```cpp
auto successCallback = [](NChannelPtr channel)
{
    std::cout << "Now connected to channel id: " << channel->id << std::endl;
};

string roomname = "MarvelMovieFans";
rtClient->joinChat(roomname, NChannelType::ROOM, true, false, successCallback);
```
{{< / code >}}

{{< code type="client" >}}
```java
String roomname = "MarvelMovieFans";
boolean persistence = true;
boolean hidden = false;
Channel channel = socket.joinChat(roomname, ChannelType.ROOM, persistence, hidden).get();
System.out.format("Now connected to channel id: %s", channel.getId());
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
var roomname = "MarvelMovieFans"
var persistence = true
var hidden = false
var type = NakamaSocket.ChannelType.Room
var channel : NakamaRTAPI.Channel = yield(socket.join_chat_async(roomname, type, persistence, hidden), "completed")

if channel.is_exception():
    print("An error occurred: %s" % channel)
    return

print("Now connected to channel id: '%s'" % [channel.id])
```
{{< / code >}}

`roomId`变量包含一个用于[发送消息](#send-messages)的ID。

### 群组

仅允许属于[群组](../groups/)的成员加入该群组聊天。消息会被实时推送给群组成员，群组成员可以读取[历史消息](#message-history)。
如果用户被踢出群组或主动退出群组，用户将不能再接收消息或读取历史消息。

用户加入群组聊天时需要具有群组ID，群组ID也可以由[用户列出](../groups/#list-groups)。

{{< missing type="client" lang="shell" />}}
{{< missing type="client" lang="bash" />}}

{{< code type="client" lang="lua" framework="defold" >}}
```lua
local group_id = "<group id>"
local persistence = true
local hidden = false
local result = socket.channel_join(group_id, socket.CHANNELTYPE_GROUP, persistence, hidden)
print("You can now send messages to channel id: " .. result.channel.id);
```
{{< / code >}}

{{< code type="client" >}}
```javascript
const groupId = "<group id>";
const persistence = true;
const hidden = false;
// 1 = Room, 2 = Direct Message, 3 = Group
const result = await socket.joinChat(3, groupId, persistence, hidden);

console.log("You can now send messages to channel id: ", result.channel.id);
```
{{< / code >}}

{{< code type="client" >}}
```csharp
var groupId = "<group id>";
var persistence = true;
var hidden = false;
var channel = await socket.JoinChatAsync(groupId, ChannelType.Group, persistence, hidden);
Console.WriteLine("You can now send messages to channel id: '{0}'", channel.Id);
```
{{< / code >}}

{{< code type="client" >}}
```cpp
auto successCallback = [](NChannelPtr channel)
{
    std::cout << "You can now send messages to channel id: " << channel->id << std::endl;
};

string groupId = "<group id>";
rtClient->joinChat(groupId, NChannelType::GROUP, true, false, successCallback);
```
{{< / code >}}

{{< code type="client" >}}
```java
String groupId = "<group id>";
boolean persistence = true;
boolean hidden = false;
Channel channel = socket.joinChat(groupId, ChannelType.GROUP, persistence, hidden).get();
System.out.format("You can now send messages to channel id %s", channel.getId());
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
var group_id = "<group id>"
var persistence = true
var hidden = false
var type = NakamaSocket.ChannelType.Group
var channel : NakamaRTAPI.Channel = yield(socket.join_chat_async(group_id, type, persistence, hidden), "completed")

if channel.is_exception():
    print("An error occurred: %s" % channel)
    return

print("Now connected to channel id: '%s'" % [channel.id])
```
{{< / code >}}

`"<group id>"`变量必须是一个用于[发送消息](#send-messages)的ID。

### 直接

用户可以通过ID直接给另一个用户发送消息。可以通过好友、群组、排行榜、配对、聊天室、以及在存储中搜索查找用户进行聊天。

只有当两个用户都加入到聊天时，两位用户才会收到实时消息。这至关重要，因为可以防止恶意用户发送垃圾邮件。

邀请其他用户直接聊天：

{{< missing type="client" lang="shell" />}}
{{< missing type="client" lang="bash" />}}

{{< code type="client" lang="lua" framework="defold" >}}
```lua
local user_id = "<user id to invite>"
local persistence = true
local hidden = false
local result = socket.channel_join(user_id, socket.CHANNELTYPE_DIRECT_MESSAGE, persistence, hidden)
print("You can now send messages to channel id: " .. result.channel.id);
```
{{< / code >}}

{{< code type="client" >}}
```javascript
const userId = "<user id to invite>";
const persistence = true;
const hidden = false;
// 1 = Room, 2 = Direct Message, 3 = Group
const result = await socket.joinChat(userId, 2, persistence, hidden);
console.log("You can now send messages to channel id:", result.channel.id);
```
{{< / code >}}

{{< code type="client" >}}
```csharp
var userId = "<user id to invite>";
var persistence = true;
var hidden = false;
var channel = await socket.JoinChatAsync(userId, ChannelType.DirectMessage, persistence, hidden);
Console.WriteLine("You can now send messages to channel id: '{0}'", channel.Id);
```
{{< / code >}}

{{< code type="client" >}}
```cpp
auto successCallback = [](NChannelPtr channel)
{
    std::cout << "You can now send messages to channel id: " << channel->id << std::endl;
};

string userId = "<user id to invite>";
rtClient->joinChat(userId, NChannelType::DIRECT_MESSAGE, true, false, successCallback);
```
{{< / code >}}

{{< code type="client" >}}
```java
String userId = "<user id to invite>";
boolean persistence = true;
boolean hidden = false;
Channel channel = socket.joinChat(userId, ChannelType.DIRECT_MESSAGE, persistence, hidden).get();
System.out.format("You can now send messages to channel id %s", channel.getId());
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
var user_id = "<user id to invite>"
var persistence = true
var hidden = false
var type = NakamaSocket.ChannelType.DirectMessage
var channel : NakamaRTAPI.Channel = yield(socket.join_chat_async(user_id, type, persistence, hidden), "completed")

if channel.is_exception():
    print("An error occurred: %s" % channel)
    return

print("Now connected to channel id: '%s'" % [channel.id])
```
{{< / code >}}

收到聊天请求时，用户会收到[应用程序内的通知](../notifications/)。如要接受邀请，用户需要向`joinChat`发来请求的用户发送同样的请求：

{{< missing type="client" lang="shell" />}}
{{< missing type="client" lang="bash" />}}

{{< code type="client" lang="lua" framework="defold" >}}
```lua
local user_id = "<user id of requestor>"
local persistence = true
local hidden = false
local result = socket.channel_join(user_id, socket.CHANNELTYPE_DIRECT_MESSAGE, persistence, hidden)
print("You can now send messages to channel id: " .. result.channel.id);
```
{{< / code >}}

{{< code type="client" >}}
```javascript
const userId = "<user id of requestor>";
const persistence = true;
const hidden = false;
// 1 = Room, 2 = Direct Message, 3 = Group
const result = await socket.joinChat(userId, 2, persistence, hidden);
console.log("You can now send messages to channel id:", result.channel.id);
```
{{< / code >}}

{{< code type="client" >}}
```csharp
var userId = "<user id of requestor>";
var persistence = true;
var hidden = false;
var channel = await socket.JoinChatAsync(userId, ChannelType.DirectMessage, persistence, hidden);
Console.WriteLine("You can now send messages to channel id: '{0}'", channel.Id);
```
{{< / code >}}

{{< code type="client" >}}
```cpp
auto successCallback = [](NChannelPtr channel)
{
    std::cout << "You can now send messages to channel id: " << channel->id << std::endl;
};

string userId = "<user id of requestor>";
rtClient->joinChat(userId, NChannelType::DIRECT_MESSAGE, true, false, successCallback);
```
{{< / code >}}

{{< code type="client" >}}
```java
String userId = "<user id of requestor>";
boolean persistence = true;
boolean hidden = false;
Channel channel = socket.joinChat(userId, ChannelType.DIRECT_MESSAGE, persistence, hidden).get();
System.out.format("You can now send messages to channel id %s", channel.getId());
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
var user_id = "<user id of requestor>"
var persistence = true
var hidden = false
var type = NakamaSocket.ChannelType.DirectMessage
var channel : NakamaRTAPI.Channel = yield(socket.join_chat_async(user_id, type, persistence, hidden), "completed")

if channel.is_exception():
    print("An error occurred: %s" % channel)
    return

print("Now connected to channel id: '%s'" % [channel.id])
```
{{< / code >}}

`"<user id>"`变量必须是一个用于[发送消息](#send-messages)的ID。

用户可以[屏蔽其他用户](../friends/#block-a-friend)，从而阻止不需要的直接消息。

## 列出在线用户

每个加入聊天的用户都会在该聊天频道显示为“在线状态”（除非作为“隐身”用户加入频道）。这些状态保存在线用户的信息。

状态是由唯一的会话和用户ID组成的。这可以在聊天频道中轻松区分从多个设备连接的同一个用户。

[加入聊天频道](#join-chat)的用户会收到聊天频道中所有其他已连接用户的初始状态列表。可使用回调从服务器接收加入和退出的用户的状态变动。这样可以轻松维护在线用户列表，并在发生变动时进行更新。

服务器经过优化，仅在其他用户加入或退出聊天时推送状态更新。

{{< missing type="client" lang="shell" />}}
{{< missing type="client" lang="bash" />}}

{{< code type="client" lang="lua" framework="defold" >}}
```lua
-- online users, keyed on user id
local online_users = {}

socket.on_channel_presence(function(presences)
    -- Remove all users who left.
    for i,user in ipairs(presences.leave) do
        online_users[user.user_id] = nil
    end
    -- Add all users who joined.
    for i,user in ipairs(presences.join) do
        online_users[user.user_id] = user
    end
end)

local roomname = "PizzaFans"
local persistence = true
local hidden = false
local result = socket.channel_join(roomname, socket.CHANNELTYPE_ROOM, persistence, hidden)

-- Setup initial online user list (excluding self).
for i,user in ipairs(result.channel.presences) do
    if user.user_id ~= result.channel.self.user_id then
        online_users[user.user_id] = user
    end
end
```
{{< / code >}}

{{< code type="client" >}}
```javascript
var onlineUsers = [];
socket.onchannelpresence = (presences) => {
  // Remove all users who left.
  onlineUsers = onlineUsers.filter((user) => {
    return !presences.leave.includes(user);
  });
  // Add all users who joined.
  onlineUsers.concat(presences.join);
};

const roomname = "PizzaFans";
const persistence = true;
const hidden = false;

// 1 = Room, 2 = Direct Message, 3 = Group
const result = await socket.joinChat(roomname, 1, persistence, hidden);

// Setup initial online user list.
onlineUsers.concat(result.channel.presences);
// Remove your own user from list.
onlineUsers = onlineUsers.filter((user) => {
  return user != channel.self;
});
```
{{< / code >}}

{{< code type="client" >}}
```csharp
var roomUsers = new List<IUserPresence>(10);
socket.ReceivedChannelPresence += presenceEvent =>
{
    foreach (var presence in presenceEvent.Leaves)
    {
        roomUsers.Remove(presence);
    }

    roomUsers.AddRange(presenceEvent.Joins);
    Console.WriteLine("Room users: [{0}]", string.Join(",\n  ", roomUsers));
};

var roomName = "PizzaFans";
var persistence = true;
var hidden = false;
var channel = await socket.JoinChatAsync(roomName, ChannelType.Room, persistence, hidden);
roomUsers.AddRange(channel.Presences);
```
{{< / code >}}

{{< code type="client" >}}
```cpp
// add this to your class: std::vector<NUserPresence> onlineUsers;

rtListener->setChannelPresenceCallback([this](const NChannelPresenceEvent& event)
{
    // Remove all users who left.
    for (auto& left : event.leaves)
    {
        for (auto it = onlineUsers.begin(); it != onlineUsers.end(); ++it)
        {
            if (it->userId == left.userId)
            {
                onlineUsers.erase(it);
                break;
            }
        }
    }

    // Add all users who joined.
    onlineUsers.insert(onlineUsers.end(), event.joins.begin(), event.joins.end());
});

auto successCallback = [this](NChannelPtr channel)
{
    onlineUsers.reserve(channel->presences.size());

    // Setup initial online user list without self.
    for (auto& joined : channel->presences)
    {
        if (joined.userId != channel->self.userId)
        {
           onlineUsers.push_back(joined);
        }
    }
};

string roomname = "PizzaFans";
rtClient->joinChat(roomname, NChannelType::ROOM, true, false, successCallback);
```
{{< / code >}}

{{< code type="client" >}}
```java
final List<UserPresence> connectedUsers = new ArrayList<UserPresence>();
SocketListener listener = new AbstractSocketListener() {
    @Override
    public void onChannelPresence(final ChannelPresenceEvent presence) {
        connectedUsers.addAll(presence.getJoins());
        for (UserPresence presence : presence.getLeaves()) {
            for (int i = 0; i < connectedUsers.size(); i++) {
                if (connectedUsers.get(i).getUserId().equals(presence.getUserId())) {
                    connectedUsers.remove(i);
                }
            }
        }
    }
};

String roomname = "PizzaFans";
boolean persistence = true;
boolean hidden = false;
Channel channel = socket.joinChat(roomname, ChannelType.ROOM, persistence, hidden);
connectedUsers.addAll(channel.getPresences());
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
var room_users = {}

func _ready():
  # First, setup the socket as explained in the authentication section.
  socket.connect("received_channel_presence", self, "_on_channel_presence")

  # Connect to the room.
  var roomname = "MarvelMovieFans"
  var persistence = true
  var hidden = false
  var type = NakamaSocket.ChannelType.Room
  var channel : NakamaRTAPI.Channel = yield(socket.join_chat_async(roomname, type, persistence, hidden), "completed")

  if channel.is_exception():
      print("An error occurred: %s" % channel)
      return

  # Add users already present in chat room.
  for p in channel.presences:
      room_users[p.user_id] = p

  print("Users in room: %s" % [room_users.keys()])

func _on_channel_presence(p_presence : NakamaRTAPI.ChannelPresenceEvent):
  for p in p_presence.joins:
      room_users[p.user_id] = p

  for p in p_presence.leaves:
      room_users.erase(p.user_id)

  print("Users in room: %s" % [room_users.keys()])
```
{{< / code >}}

## 接收消息

用户加入聊天频道时开始实时接收消息。事件处理器接收新消息，这些消息可以添加到您的UI。消息按照服务器处理的顺序传递。

{{< missing type="client" lang="shell" />}}
{{< missing type="client" lang="bash" />}}

{{< code type="client" lang="lua" framework="defold" >}}
```lua
socket.on_channel_message(function(message)
  print("Received a message on channel: " .. message.channel_id);
  print("Message content: " .. message.content);
end)
```
{{< / code >}}

{{< code type="client" >}}
```javascript
socket.onchannelmessage = (message) => {
  console.log("Received a message on channel: %o", message.channel_id);
  console.log("Message content: %o", message.content);
};
```
{{< / code >}}

{{< code type="client" >}}
```csharp
socket.ReceivedChannelMessage += message =>
{
    Console.WriteLine("Received: {0}", message);
    Console.WriteLine("Message has channel id: {0}", message.ChannelId);
    Console.WriteLine("Message content: {0}", message.Content);
};
```
{{< / code >}}

{{< code type="client" >}}
```cpp
rtListener->setChannelMessageCallback([](const NChannelMessage& msg)
{
    // msg.content is JSON string
    std::cout << "OnChannelMessage " << msg.content << std::cout;
});
```
{{< / code >}}

{{< code type="client" >}}
```java
SocketListener listener = new AbstractSocketListener() {
    @Override
    public void onChannelMessage(final ChannelMessage message) {
        System.out.format("Received a message on channel %s", message.getChannelId());
        System.out.format("Message content: %s", message.getContent());
    }
};
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
func _ready():
    # First, setup the socket as explained in the authentication section.
    socket.connect("received_channel_message", self, "_on_channel_message")

func _on_channel_message(p_message : NakamaAPI.ApiChannelMessage):
    print(p_message)
    print("Received a message on channel: %s" % [p_message.channel_id])
    print("Message content: %s" % [p_message.content])
```
{{< / code >}}

### 群组

在群组聊天中，用户将收到来自服务器的其他消息。这些消息包含一些事件，例如用户加入或离开群组的事件，某人被提升为管理员的事件，等等。您可能希望用户在聊天流中看到这些消息，或在UI中忽略这些消息。

您可以通过信息的 从聊天信息中识别出事件信息 `Type`。

{{< missing type="client" lang="shell" />}}
{{< missing type="client" lang="bash" />}}

{{< code type="client" lang="lua" framework="defold" >}}
```lua
if message.code ~= 0 then
  print("Received message with code: " .. message.code);
end
```
{{< / code >}}

{{< code type="client" >}}
```javascript
if (message.code != 0) {
  console.log("Received message with code:", message.code);
}
```
{{< / code >}}

{{< code type="client" >}}
```csharp
if (message.Code != 0)
{
    Console.WriteLine("Received message with code '{0}'", message.Code);
}
```
{{< / code >}}

{{< code type="client" >}}
```cpp
if (msg.code != 0)
{
    std::cout << "Received message with code: " << msg.code << std::endl;
}
```
{{< / code >}}

{{< code type="client" >}}
```java
if (message.getCode() != 0) {
    System.out.println("Received message with code %s", message.getCode());
}
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
if p_message.code != 0:
    print("Received message with code:", p_message.code)
```
{{< / code >}}

{{< table name="nakama.concepts.chat.message-codes" >}}

## 发送消息

用户[加入聊天频道](#join-chat)后，其ID可用于发送带有JSON编码字符串的消息。

每条发送的信息在被服务器收到后都会返回一条确认消息。返回的确认消息包含消息ID、时间戳和发送该消息的用户的详细信息。

{{< missing type="client" lang="shell" />}}
{{< missing type="client" lang="bash" />}}

{{< code type="client" lang="lua" framework="defold" >}}
```lua
local channel_id = "<ChannelId>"
local content = { some = "Data" }
local message_ack = socket.channel_message_send(channel_id, json.encode(content))
```
{{< / code >}}

{{< code type="client" >}}
```javascript
var channelId = "<channel id>";
var data = { "some": "data" };
const messageAck = await socket.writeChatMessage(channelId, data);
```
{{< / code >}}

{{< code type="client" >}}
```csharp
var channelId = "<channel id>";
var content = new Dictionary<string, string> {{"hello", "world"}}.ToJson();
var sendAck = await socket.WriteChatMessageAsync(channelId, content);
```
{{< / code >}}

{{< code type="client" >}}
```cpp
auto successCallback = [](const NChannelMessageAck& ack)
{
    std::cout << "message id: " << ack.messageId << std::endl;
};

string channelId = "<channel id>";
string data = "{ \"some\": \"data\" }";
rtClient->writeChatMessage(channelId, data, successCallback);
```
{{< / code >}}

{{< code type="client" >}}
```java
String channelId = "<channel id>";
final String content = "{\"message\":\"Hello world\"}";
ChannelMessageAck sendAck = socket.writeChatMessage(channelId, content).get();
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
var channel_id = "<channel id>"
var data = { "some": "data" }
var message_ack : NakamaRTAPI.ChannelMessageAck = yield(socket.write_chat_message_async(channel_id, data), "completed")

if message_ack.is_exception():
    print("An error occurred: %s" % message_ack)
    return

print("Sent message %s" % [message_ack])
```
{{< / code >}}

### 过滤消息内容

通常会过滤聊天频道中的用户消息，防止辱骂性或攻击性行为。在Nakama中，这可以通过在您的[服务器运行代码](../../server-framework/)中使用[钩子](../../server-framework/introduction/#hooks)来实现，从而净化或拒绝任何含有不适当内容的消息。

发送消息时，会在服务器上触发这段代码：

{{< missing type="server" lang="lua" />}}

{{< code type="server" >}}
```go
// Inside your Go module's `InitModule` function.
if err := initializer.RegisterBeforeRt("ChannelMessageSend", func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, envelope *rtapi.Envelope) (*rtapi.Envelope, error) {
  message := envelope.GetChannelMessageSend()
  if strings.Contains(message.Content, "bad word") {
    // Alternatively, to sanitize instead of reject:
    // message.Content = strings.ReplaceAll(message.Content, "bad word", "****")

    // Reject the message send:
    return nil, runtime.NewError("profanity detected", 3)
  }
  return envelope, nil
}); err != nil {
  return err
}
```
{{< / code >}}

{{< code type="server" >}}
```typescript
let rtBeforeChannelMessageSend: nkruntime.RtBeforeHookFunction<nkruntime.Envelope> = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, envelope: nkruntime.Envelope) : nkruntime.Envelope {
    let e = envelope as nkruntime.EnvelopeChannelMessageSend;
    if (e == null)
    {
        return e;
    }

    if (e.channelMessageSend.content.indexOf('Bad Word') !== -1) {
        // Alternatively, to sanitize instead of reject:
        //e.channelMessageSend.content = e.channelMessageSend.content.replace('Bad Word', '****');

        // Reject the message send.
        throw new Error("Profanity detected");
    }

    return e;
}

initializer.registerRtBefore("ChannelMessageSend", rtBeforeChannelMessageSend);
```
{{< / code >}}

在上述示例中，您正在创建和维护自己的过滤标准（“异常词汇”索引），但如果需要，您也可以选择与任何第三方过滤API集成。

<!--
### Shadow banning

When forced to deal with consistently problematic users and their offensive or abusive conduct, "shadow banning" is one option available prior to [banning the user entirely](../friends/#ban-a-user). When shadow-banned, a user is still able to read and send channel messages, but their messages are not visible to the other users in the chat channel.

// Code samples by Tom :)
-->

## 退出聊天

用户退出聊天频道时将不再实时接收消息。这可能有助于当处于UI的其他部分时，将聊天“静音”。

{{< missing type="client" lang="shell" />}}
{{< missing type="client" lang="bash" />}}

{{< code type="client" lang="lua" framework="defold" >}}
```lua
local channel_id = "<ChannelId>"
socket.channel_leave(channel_id)
```
{{< / code >}}

{{< code type="client" >}}
```javascript
var channelId = "<channel id>";
await socket.leaveChat(channelId);
```
{{< / code >}}

{{< code type="client" >}}
```csharp
var channelId = "<channel id>";
await socket.LeaveChatAsync(channelId);
```
{{< / code >}}

{{< code type="client" >}}
```cpp
string channelId = "<channel id>";
rtClient->leaveChat(channelId);
```
{{< / code >}}

{{< code type="client" >}}
```java
String channelId = "<channel id>";
socket.leaveChat(channelId).get();
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
var channel_id = "<channel id>"
var result : NakamaAsyncResult = yield(socket.leave_chat_async(channel_id), "completed")

if result.is_exception():
    print("An error occurred: %s" % result)
    return

print("Left chat")
```
{{< / code >}}

## 消息历史

每个聊天对话都会存储历史消息（除非持久性被设置为false）。历史消息还包含服务器向群组聊天频道发送的[事件消息](#receive-messages)。每个用户都可以在下次在线连接时检索频道的过往消息。
用户无需加入聊天频道即可查看聊天历史记录。这可以帮助用户无须在线聊天即可“偷看”过往消息。

消息的排列方式可以为从最近到最早，也可以相反（即从最早到最近）。消息将分批返回，每批最多100条，如果多于100条，每条消息都会使用游标。

{{< code type="client" lang="lua" framework="defold" >}}
```lua
local limit = 10
local forward = true
local channel_id = "<channel id>"
local result = nakama.list_channel_messages(client, channel_id, limit, forward)

for i,message in ipairs(result.messages) do
  print(("Message id %s and content %s"):format(message.message_id, message.content))
end
```
{{< / code >}}

{{< code type="client" >}}
```bash
curl -X GET "http://127.0.0.1:7350/v2/channel/<channelId>" \
  -H 'authorization: Bearer <session token>'
```
{{< / code >}}

{{< code type="client" >}}
```javascript
const channelId = "<channel id>";
const result = await client.listChannelMessages(session, channelId, 10);

result.messages.forEach((message) => {
  console.log("Message has id %o and content %o", message.message_id, message.data);
});

console.log("Get the next page of messages with the cursor:", result.next_cursor);
```
{{< / code >}}

{{< code type="client" >}}
```csharp
var channelId = "<channel id>";
var result = await client.ListChannelMessagesAsync(session, channelId, 10, true);

foreach (var m in result.Messages)
{
    Console.WriteLine("Message id '{0}' content '{1}'", m.MessageId, m.Content);
}
```
{{< / code >}}

{{< code type="client" >}}
```cpp
auto successCallback = [](NChannelMessageListPtr list)
{
    for (auto& message : list->messages)
    {
        std::cout << "message content: " << message.content << std::endl;
    }

    std::cout << "Get the next page of messages with the cursor: " << list->nextCursor << std::endl;
};

string channelId = "<channel id>";
client->listChannelMessages(session, channelId, 10, opt::nullopt, opt::nullopt, successCallback);
```
{{< / code >}}

{{< code type="client" >}}
```java
String channelId = "<channel id>";
ChannelMessageList messages = client.listChannelMessages(session, channelId, 10).get();

for (ChannelMessage message : messages.getMessagesList()) {
    System.out.format("Message content: %s", message.getContent());
}
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
var channel_id = "<channel id>"
var result : NakamaAPI.ApiChannelMessageList = yield(client.list_channel_messages_async(session, channel_id, 10), "completed")

if result.is_exception():
    print("An error occurred: %s" % result)
    return

for m in result.messages:
    var message : NakamaAPI.ApiChannelMessage = m as NakamaAPI.ApiChannelMessage
    print("Message has id %s and content %s" % [message.message_id, message.content])

print("Get the next page of messages with the cursor: %s" % [result.next_cursor])
```
{{< / code >}}

{{< code type="client" >}}
```shell
GET /v2/channel/<channelId>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
```
{{< / code >}}

可以使用游标在一批消息的最后进行分页，以查看下一组结果。

我们建议您在UI中只列出最近的100条消息。当用户滚动到您的UI面板的底部时，获取下一组100条更早的消息，这样可以带来良好的用户体验。

{{< code type="client" lang="lua" framework="defold" >}}
```lua
local limit = 10
local forward = true
local channel_id = "<channel id>"
local result = nakama.list_channel_messages(client, channel_id, limit, forward)

for i,message in ipairs(result.messages) do
  print(("Message id %s and content %s"):format(message.message_id, message.content))
end

if result.nextCursor then
    -- Get the next 10 messages
    local result = nakama.list_channel_messages(client, channel_id, limit, forward, result.nextCursor)

    for i,message in ipairs(result.messages) do
      print(("Message id %s and content %s"):format(message.message_id, message.content))
    end
end
```
{{< / code >}}

{{< code type="client" >}}
```bash
curl -X GET "http://127.0.0.1:7350/v2/channel/<channelId>?forward=true&limit=10&cursor=<cursor>" \
  -H 'Authorization: Bearer <session token>'
```
{{< / code >}}

{{< code type="client" >}}
```javascript
var channelId = "<channel id>";
var forward = true;
var result = await client.listChannelMessages(session, channelId, 10, forward);

result.messages.forEach((message) => {
  console.log("Message has id %o and content %o", message.message_id, message.data);
});

if (result.next_cursor) {
  // Get the next 10 messages.
  var result = await client.listChannelMessages(session, channelId, 10, forward, result.next_cursor);

  result.messages.forEach((message) => {
    console.log("Message has id %o and content %o", message.message_id, message.data);
  });
}
```
{{< / code >}}

{{< code type="client" >}}
```csharp
var channelId = "<channel id>";
var result = await client.ListChannelMessagesAsync(session, channelId, 10, true);

foreach (var m in result.Messages)
{
    Console.WriteLine("Message id '{0}' content '{1}'", m.MessageId, m.Content);
}

if (!string.IsNullOrEmpty(result.NextCursor)) {
    // Get the next 10 messages.
    var result = await client.ListChannelMessagesAsync(session, channelId, 10, true, result.NextCursor);
    foreach (var m in messages)
    {
        Console.WriteLine("Message id '{0}' content '{1}'", m.MessageId, m.Content);
    }
};
```
{{< / code >}}

{{< code type="client" >}}
```cpp
void YourClass::listChannelMessages(const std::string& cursor)
{
    auto successCallback = [this](NChannelMessageListPtr list)
    {
        for (auto& message : list->messages)
        {
            std::cout << "message content: " << message.content << std::endl;
        }

        if (!list->nextCursor.empty())
        {
            listChannelMessages(list->nextCursor);
        }
    };

    string channelId = "<channel id>";
    client->listChannelMessages(session, channelId, 10, cursor, true, successCallback);
}

listChannelMessages("");
```
{{< / code >}}

{{< code type="client" >}}
```java
String channelId = "<channel id>";
ChannelMessageList messages = client.listChannelMessages(session, channelId, 10).get();

if (messages.getNextCursor() != null) {
    messages = client.listChannelMessages(session, channelId, 10, messages.getNextCursor()).get();

    for (ChannelMessage message : messages.getMessagesList()) {
        System.out.format("Message content: %s", message.getContent());
    }
  }
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
var channel_id = "<channel id>"
var forward = true
var result : NakamaAPI.ApiChannelMessageList = yield(client.list_channel_messages_async(session, channel_id, 10, forward), "completed")

if result.is_exception():
    print("An error occurred: %s" % result)
    return

for m in result.messages:
    var message : NakamaAPI.ApiChannelMessage = m as NakamaAPI.ApiChannelMessage
    print("Message has id %s and content %s" % [message.message_id, message.content])

if result.next_cursor:
    result = yield(client.list_channel_messages_async(session, channel_id, 10, forward, result.next_cursor), "completed")

    if result.is_exception():
        print("An error occurred: %s" % result)
        return

    for m in result.messages:
        var message : NakamaAPI.ApiChannelMessage = m as NakamaAPI.ApiChannelMessage
        print("Message has id %s and content %s" % [message.message_id, message.content])
```
{{< / code >}}

{{< code type="client" >}}
```shell
GET /v2/channel/<channel id>?forward=true&limit=10&cursor=<cursor>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
```
{{< / code >}}

### 可缓存游标

如果只检索客户最后一次检索列表后添加的信息，会很有帮助。这可以通过每个频道消息返回的可缓存游标来完成。通过新的列出操作发送游标将只检索比所看到的更新的消息。

可缓存游标标记最近检索到的频道消息的位置。我们建议您将可缓存游标存储在设备存储器中，并在客户端下次请求最近的通知时使用它。

{{< missing type="client" lang="shell" />}}
{{< missing type="client" lang="bash" />}}

{{< code type="client" lang="lua" framework="defold" >}}
```lua
local limit = 10
local forward = true
local channel_id = "<channel id>"
local cacheable_cursor = "<cacheableCursor>";
local result = nakama.list_channel_messages(client, channel_id, limit, forward, cacheable_cursor)

for i,message in ipairs(result.messages) do
  print(("Message id %s and content %s"):format(message.message_id, message.content))
end
```
{{< / code >}}

{{< code type="client" >}}
```javascript
const cacheableCursor = "<cacheableCursor>";
const channelId = "<channelId>";
const result = await client.listChannelMessages(channelId, 10, cacheableCursor);
result.messages.forEach((message) => {
  console.log("Message has id %o and content %o", message.message_id, message.data);
});
```
{{< / code >}}

{{< code type="client" >}}
```csharp
const string cacheableCursor = "<cacheableCursor>";
const string channelId = "<channelId>";
var result = await client.ListChannelMessagesAsync(session, channelId, 10, true, cacheableCursor);
foreach (var m in result.Messages)
{
    Console.WriteLine("Message id '{0}' content '{1}'", m.MessageId, m.Content);
}
```
{{< / code >}}

{{< code type="client" >}}
```cpp
auto successCallback = [this](NChannelMessageListPtr list)
{
    for (auto& message : list->messages)
    {
        std::cout << "Message content " << message.content << std::endl;
    }
};

string channelId = "<channelId>";
string cacheableCursor = "<cacheableCursor>";
client->listChannelMessages(session, channelId, 10, cacheableCursor, true, successCallback);
```
{{< / code >}}

{{< code type="client" >}}
```java
String channelId = "<channel id>";
String cacheableCursor = "<cacheableCursor>";
ChannelMessageList messages = client.listChannelMessages(session, channelId, 10, cacheableCursor).get();
for (ChannelMessage message : messages.getMessagesList()) {
        System.out.format("Message content: %s", message.getContent());
    }
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
var cacheable_cursor = "<cacheable cursor>";
var channel_id = "<channel id>"
var forward = true
var result : NakamaAPI.ApiChannelMessageList = yield(client.list_channel_messages_async(session, channel_id, 10, forward, cacheable_cursor), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
for m in result.messages:
    var message : NakamaAPI.ApiChannelMessage = m as NakamaAPI.ApiChannelMessage
    print("Message has id %s and content %s" % [message.message_id, message.content])
```
{{< / code >}}
