实时聊天 #

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

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

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

聊天频道 #

有3种类型的频道:

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

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

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

持续性 #

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

如果消息仅应发送给在线用户,而不应保存在消息历史记录中,则客户端可以在加入频道时禁用维持。

隐藏的频道成员 #

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

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

加入聊天 #

要向其他用户发送消息,用户必须加入他们想要在其中交流的聊天频道。这也将使用户能够实时接收到消息

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

房间 #

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

Code snippet for this language REST has not been found. Please choose another language to show equivalent examples.
Code snippet for this language cURL has not been found. Please choose another language to show equivalent examples.

Client
1
2
3
4
5
6
7
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);
Client
1
2
3
4
5
6
7
8
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);
Client
1
2
3
4
5
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);
Client
1
2
3
4
5
6
7
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);
Client
1
2
3
4
5
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());
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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])

roomId变量包含一个用于发送消息的ID。

群组 #

仅允许属于群组的成员加入该群组聊天。消息会被实时推送给群组成员,群组成员可以读取历史消息。 如果用户被踢出群组或主动退出群组,用户将不能再接收消息或读取历史消息。

用户加入群组聊天时需要具有群组ID,群组ID也可以由用户列出

Code snippet for this language REST has not been found. Please choose another language to show equivalent examples.
Code snippet for this language cURL has not been found. Please choose another language to show equivalent examples.

Client
1
2
3
4
5
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);
Client
1
2
3
4
5
6
7
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);
Client
1
2
3
4
5
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);
Client
1
2
3
4
5
6
7
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);
Client
1
2
3
4
5
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());
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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])

"<group id>"变量必须是一个用于发送消息的ID。

直接 #

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

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

邀请其他用户直接聊天:

Code snippet for this language REST has not been found. Please choose another language to show equivalent examples.
Code snippet for this language cURL has not been found. Please choose another language to show equivalent examples.

Client
1
2
3
4
5
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);
Client
1
2
3
4
5
6
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);
Client
1
2
3
4
5
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);
Client
1
2
3
4
5
6
7
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);
Client
1
2
3
4
5
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());
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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])

收到聊天请求时,用户会收到应用程序内的通知。如要接受邀请,用户需要向joinChat发来请求的用户发送同样的请求:

Code snippet for this language REST has not been found. Please choose another language to show equivalent examples.
Code snippet for this language cURL has not been found. Please choose another language to show equivalent examples.

Client
1
2
3
4
5
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);
Client
1
2
3
4
5
6
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);
Client
1
2
3
4
5
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);
Client
1
2
3
4
5
6
7
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);
Client
1
2
3
4
5
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());
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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])

"<user id>"变量必须是一个用于发送消息的ID。

用户可以屏蔽其他用户,从而阻止不需要的直接消息。

列出在线用户 #

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

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

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

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

Code snippet for this language REST has not been found. Please choose another language to show equivalent examples.
Code snippet for this language cURL has not been found. Please choose another language to show equivalent examples.

Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
-- 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
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;
});
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 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);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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());
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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()])

接收消息 #

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

Code snippet for this language REST has not been found. Please choose another language to show equivalent examples.
Code snippet for this language cURL has not been found. Please choose another language to show equivalent examples.

Client
1
2
3
4
socket.on_channel_message(function(message)
  print("Received a message on channel: " .. message.channel_id);
  print("Message content: " .. message.content);
end)
Client
1
2
3
4
socket.onchannelmessage = (message) => {
  console.log("Received a message on channel: %o", message.channel_id);
  console.log("Message content: %o", message.content);
};
Client
1
2
3
4
5
6
socket.ReceivedChannelMessage += message =>
{
    Console.WriteLine("Received: {0}", message);
    Console.WriteLine("Message has channel id: {0}", message.ChannelId);
    Console.WriteLine("Message content: {0}", message.Content);
};
Client
1
2
3
4
5
rtListener->setChannelMessageCallback([](const NChannelMessage& msg)
{
    // msg.content is JSON string
    std::cout << "OnChannelMessage " << msg.content << std::cout;
});
Client
1
2
3
4
5
6
7
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());
    }
};
Client
1
2
3
4
5
6
7
8
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])

群组 #

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

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

Code snippet for this language REST has not been found. Please choose another language to show equivalent examples.
Code snippet for this language cURL has not been found. Please choose another language to show equivalent examples.

Client
1
2
3
if message.code ~= 0 then
  print("Received message with code: " .. message.code);
end
Client
1
2
3
if (message.code != 0) {
  console.log("Received message with code:", message.code);
}
Client
1
2
3
4
if (message.Code != 0)
{
    Console.WriteLine("Received message with code '{0}'", message.Code);
}
Client
1
2
3
4
if (msg.code != 0)
{
    std::cout << "Received message with code: " << msg.code << std::endl;
}
Client
1
2
3
if (message.getCode() != 0) {
    System.out.println("Received message with code %s", message.getCode());
}
Client
1
2
if p_message.code != 0:
    print("Received message with code:", p_message.code)
CodePurposeSourceDescription
0Chat MessageUser用户发送的所有消息。
1Chat UpdateUser用户正在更新先前发送的消息。
2Chat RemoveUser用户正在移除先前发送的消息。
3Joined GroupServer用户加入群组时的事件消息。
4Added to GroupServer用户被加入到群组时的事件消息。
5Left GroupServer用户离开群组时的事件消息。
6Kicked from GroupServer管理员将用户踢出群组时的事件消息。
7Promoted in GroupServer用户升级为群组管理员时的事件消息。
8Banned in GroupServer用户被禁止进入群组的事件消息。
9Demoted in GroupServer用户在群组中降级时的事件消息。

发送消息 #

用户加入聊天频道后,其ID可用于发送带有JSON编码字符串的消息。

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

Code snippet for this language REST has not been found. Please choose another language to show equivalent examples.
Code snippet for this language cURL has not been found. Please choose another language to show equivalent examples.

Client
1
2
3
local channel_id = "<ChannelId>"
local content = { some = "Data" }
local message_ack = socket.channel_message_send(channel_id, json.encode(content))
Client
1
2
3
var channelId = "<channel id>";
var data = { "some": "data" };
const messageAck = await socket.writeChatMessage(channelId, data);
Client
1
2
3
var channelId = "<channel id>";
var content = new Dictionary<string, string> {{"hello", "world"}}.ToJson();
var sendAck = await socket.WriteChatMessageAsync(channelId, content);
Client
1
2
3
4
5
6
7
8
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);
Client
1
2
3
String channelId = "<channel id>";
final String content = "{\"message\":\"Hello world\"}";
ChannelMessageAck sendAck = socket.writeChatMessage(channelId, content).get();
Client
1
2
3
4
5
6
7
8
9
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])

过滤消息内容 #

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

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

Code snippet for this language Lua has not been found. Please choose another language to show equivalent examples.
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 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
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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);

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

退出聊天 #

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

Code snippet for this language REST has not been found. Please choose another language to show equivalent examples.
Code snippet for this language cURL has not been found. Please choose another language to show equivalent examples.

Client
1
2
local channel_id = "<ChannelId>"
socket.channel_leave(channel_id)
Client
1
2
var channelId = "<channel id>";
await socket.leaveChat(channelId);
Client
1
2
var channelId = "<channel id>";
await socket.LeaveChatAsync(channelId);
Client
1
2
string channelId = "<channel id>";
rtClient->leaveChat(channelId);
Client
1
2
String channelId = "<channel id>";
socket.leaveChat(channelId).get();
Client
1
2
3
4
5
6
7
8
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")

消息历史 #

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

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

Client
1
2
3
4
5
6
7
8
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
Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/channel/<channelId>" \
  -H 'authorization: Bearer <session token>'
Client
1
2
3
4
5
6
7
8
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);
Client
1
2
3
4
5
6
7
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);
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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);
Client
1
2
3
4
5
6
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());
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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])
Client
1
2
3
4
5
GET /v2/channel/<channelId>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>

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

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

Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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
Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/channel/<channelId>?forward=true&limit=10&cursor=<cursor>" \
  -H 'Authorization: Bearer <session token>'
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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);
  });
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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);
    }
};
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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("");
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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());
    }
  }
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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])
Client
1
2
3
4
5
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 snippet for this language REST has not been found. Please choose another language to show equivalent examples.
Code snippet for this language cURL has not been found. Please choose another language to show equivalent examples.

Client
1
2
3
4
5
6
7
8
9
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
Client
1
2
3
4
5
6
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);
});
Client
1
2
3
4
5
6
7
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);
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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);
Client
1
2
3
4
5
6
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());
    }
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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])