Real-time Chat #

Real-time chat makes it easy to power a live community.

Users can chat with each other 1-on-1, as part of a group, and in chat rooms. Messages can contain images, links, and other content. These messages are delivered immediately to clients if the recipients are online and stored in message history so offline users can catch up when they connect.

Every message which flows through the real-time chat engine belongs to a channel which is used internally to identify which users should receive the messages. Users explicitly join and leave channels when they connect. This makes it easy to selectively listen for messages which they care about or decide to “mute” certain channels when they’re busy. Users can also join multiple channels at once to chat simultaneously in multiple groups or chat rooms.

Chat channels #

There are 3 types of channels:

  1. A chat room is great for public chat. Any user can join and participate without the need for permission. These rooms can scale to millions of users all in simultaneous communication. This is perfect for live participation apps or games with live events or tournaments.

  2. A group chat is private to only users part of a group. Each user must be a member of the group and no other users can participate. You can use group chat with team-based gameplay or collaboration.

  3. Direct chat is private between two users. Each user will receive a notification when they’ve been invited to chat. Both users must join for messages to be exchanged which prevents spam from bad users.

Persistence #

By default all channels are persistent, so messages sent through them are saved to the database and available in message history later. This history can be used by offline users to catch up with messages they’ve missed the next time they connect.

If messages should only be sent to online users and never kept in message history, clients can join channels with persistence disabled.

Hidden channel members #

By default, all users joining a channel are visible to other users. Existing channel participants will receive an event when the user connects and disconnects, and new channel joiners will receive a list of users already in the channel.

Users can opt to hide their channel presence when connecting, so they will not generate join/leave notifications and will not appear in listings of channel members. They will still be able to send and receive real-time messages as normal.

Join chat #

To send messages to other users a user must join the chat channel they want to communicate on. This will also enable messages to be received in real-time.

Each user can join many rooms, groups, and direct chats with their session. The same user can also be connected to the same chats from other devices because each device is identified as a separate session.

Rooms #

A room is created dynamically for users to chat. A room has a name and will be set up on the server when any user joins. The list of room names available to join can be stored within client code or via remote configuration with a storage record.

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(roomname, 1, 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
let roomname = "MarvelMovieFans"
let persistence = true
let hidden = false
let channel = try await socket.joinChat(target: roomname, type: .room, persistence: persistence, hidden: hidden)
print("Now connected to channel id: \(channel.id)")
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const roomname = 'MarvelMovieFans';
const persistence = true;
const hidden = false;
final channel = await socket.joinChannel(
  target: roomname,
  type: ChannelType.room,
  persistence: persistence,
  hidden: hidden,
);
print("Now connected to channel id: '${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])
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 = await socket.join_chat_async(roomname, type, persistence, hidden)

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

print("Now connected to channel id: '%s'" % [channel.id])

The roomId variable contains an ID used to send messages.

Groups #

A group chat can only be joined by a user who is a member of the group. Messages are pushed in real-time to group members and they can read historic messages. If a user is kicked or leaves a group they can no longer receive messages or read history.

A group ID is needed when a user joins group chat and can be listed by the user.

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
let groupId = "<group id>"
let persistence = true
let hidden = false
let channel = try await socket.joinChat(target: groupId, type: .group, persistence: persistence, hidden: hidden)
print("You can now send messages to channel id: \(channel.id)")
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const groupId = '<group id>';
const persistence = true;
const hidden = false;
final channel = await socket.joinChannel(
  target: groupId,
  type: ChannelType.group,
  persistence: persistence,
  hidden: hidden,
);
print("You can now send messages to channel id: '${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])
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 = await socket.join_chat_async(group_id, type, persistence, hidden)

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

print("Now connected to channel id: '%s'" % [channel.id])

The "<group id>" variable must be an ID used to send messages.

Direct #

Users can direct message another user by ID. Friends, groups, leaderboards, matchmaker, room chat, and searches in storage are all ways to find users for chat.

Neither user will receive messages in real-time until both users have joined the chat. This is important because it prevents spam messages from bad users.

To invite another user to a direct chat:

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
let userId = "<user id to invite>"
let persistence = true
let hidden = false
let channel = try await socket.joinChat(target: userId, type: .directMessage, persistence: persistence, hidden: hidden)
print("You can now send messages to channel id: \(channel.id)")
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const userId = '<user id to invite>';
const persistence = true;
const hidden = false;
final channel = await socket.joinChannel(
  target: userId,
  type: ChannelType.directMessage,
  persistence: persistence,
  hidden: hidden,
);
print("You can now send messages to channel id: '${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])
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 = await socket.join_chat_async(user_id, type, persistence, hidden)

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

print("Now connected to channel id: '%s'" % [channel.id])

Users will receive an in-app notification when a request to chat has been received. To accept the invitation users respond with the same joinChat request targeting the requesting user:

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
let userId = "<user id of requestor>"
let persistence = true
let hidden = false
let channel = try await socket.joinChat(target: userId, type: .directMessage, persistence: persistence, hidden: hidden)
print("You can now send messages to channel id: \(channel.id)")
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const userId = '<user id of requestor>';
const persistence = true;
const hidden = false;
final channel = await socket.joinChannel(
  target: userId,
  type: ChannelType.directMessage,
  persistence: persistence,
  hidden: hidden,
);
print("You can now send messages to channel id: '${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])
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 = await socket.join_chat_async(user_id, type, persistence, hidden)

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

print("Now connected to channel id: '%s'" % [channel.id])

The "<user id>" variable must be an ID used to send messages.

A user can block other users to stop unwanted direct messages.

List online users #

Each user who joins a chat becomes a “presence” in the chat channel (unless they’ve joined as a “hidden” channel user). These presences keep information about which users are online.

A presence is made up of a unique session combined with a user ID. This makes it easy to distinguish between the same user connected from multiple devices in the chat channel.

The user who joins a chat channel receives an initial presence list of all other connected users in the chat channel. A callback can be used to receive presence changes from the server about users who join and leave. This makes it easy to maintain a list of online users and update it when changes occur.

The server is optimized to only push presence updates when other users join or leave the chat.

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
var roomUsers = [UserPresence]()
socket.onChannelPresence = { event in
    // Remove all users who left.
    for left in event.leaves {
        roomUsers.removeAll { $0.userId == left.userId }
    }

    // Add all users who joined.
    roomUsers.append(contentsOf: event.joins.map { $0.toUserPresence() })
    print("Room users: \(roomUsers)")
}

let roomName = "PizzaFans"
let persistence = true
let hidden = false
let channel = try await socket.joinChat(target: roomName, type: .room, persistence: persistence, hidden: hidden)
roomUsers.append(contentsOf: channel.presences)
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
final List<UserPresence> roomUsers = [];
socket.onChannelPresence.listen((event) {
// Remove all users who left.
roomUsers.removeWhere((user) => event.leaves.contains(user));
// Add all users who joined.
roomUsers.addAll(event.joins);
print('Room users: $roomUsers');
});

const roomName = 'PizzaFans';
const persistence = true;
const hidden = false;
final channel = await socket.joinChannel(
  target: roomName,
  type: ChannelType.room,
  persistence: persistence,
  hidden: hidden,
);
roomUsers.addAll(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()])
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.received_channel_presence.connect(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 = await socket.join_chat_async(roomname, type, persistence, hidden)

  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()])

Receive messages #

A user joins a chat channel to start receiving messages in real-time. Each new message is received by an event handler and can be added to your UI. Messages are delivered in the order they are handled by the server.

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
socket.onChannelMessage = { message in
    print("Received a message on channel: \(message.channelID)")
    print("Message content: \(message.content)")
}
Client
1
2
3
4
socket.onChannelMessage.listen((message) {
  print('Received a message on channel: ${message.channelId}');
  print('Message content: ${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])
Client
1
2
3
4
5
6
7
8
func _ready():
    # First, setup the socket as explained in the authentication section.
    socket.received_channel_message.connect(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])

Groups #

In group chat a user will receive other messages from the server. These messages contain events on users who join or leave the group, when someone is promoted as an admin, etc. You may want users to see these messages in the chat stream or ignore them in the UI.

You can identify event messages from chat messages by the message 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
if message.code != 0 {
    print("Received message with code \(message.code)")
}
Client
1
2
3
if (message.code != 0) {
  print('Received message with code: ${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)
Client
1
2
if p_message.code != 0:
    print("Received message with code:", p_message.code)
CodePurposeSourceDescription
0Chat MessageUserAll messages sent by users.
1Chat UpdateUserA user updating a message they previously sent.
2Chat RemoveUserA user removing a message they previously sent.
3Joined GroupServerAn event message for when a user joined the group.
4Added to GroupServerAn event message for when a user was added to the group.
5Left GroupServerAn event message for when a user left a group.
6Kicked from GroupServerAn event message for when an admin kicked a user from the group.
7Promoted in GroupServerAn event message for when a user is promoted as a group admin.
8Banned in GroupServerAn event message for when a user got banned from a group.
9Demoted in GroupServerAn event message for when a user got demoted in a group.

Send messages #

When a user has joined a chat channel, its ID can be used to send messages with JSON encoded strings.

Every message sent returns an acknowledgment when it’s received by the server. The acknowledgment returned contains a message ID, timestamp, and details back about the user who sent it.

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
let channelId = "<channel id>"
let jsonContent = "\"message\": \"Hello world\""
let messageAck = try await socket.writeChatMessage(channelId: channelId, content: jsonContent)
Client
1
2
3
4
5
6
7
const channelId = '<channel id>';
final content = { 'message': 'Hello world' };
final sendAck = await socket.sendMessage(
  channelId: channelId,
  content: content,
);
print('Message sent: ${sendAck.messageId}');
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])
Client
1
2
3
4
5
6
7
8
9
var channel_id = "<channel id>"
var data = { "some": "data" }
var message_ack : NakamaRTAPI.ChannelMessageAck = await socket.write_chat_message_async(channel_id, data)

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

print("Sent message %s" % [message_ack])

Filtering message content #

It is common to implement filtering of user messages in chat channels to prevent abusive or offensive conduct. This can be achieved in Nakama by using hooks in your server runtime code to either sanitize or reject any message containing inappropriate content.

This code triggers on the server when a message is sent:

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);

In the above example you are creating and maintaining your own filtering criteria (the “Bad Word” index), but you can also elect to integrate with any third-party filtering API if desired.

Leave chat #

A user can leave a chat channel to no longer be sent messages in real-time. This can be useful to “mute” a chat while in some other part of the 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
let channelId = "<channel id>"
try await socket.leaveChat(channelId: channelId)
Client
1
2
const channelId = '<channel id>';
await socket.leaveChannel(channelId: 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")
Client
1
2
3
4
5
6
7
8
var channel_id = "<channel id>"
var result : NakamaAsyncResult = await socket.leave_chat_async(channel_id)

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

print("Left chat")

Message history #

Every chat conversation stores a history of messages (unless persistence is set false). The history also contains event messages sent by the server to group chat channels. Each user can retrieve old messages for channels when they next connect online. A user does not have to join a chat channel to see chat history. This is useful to “peek” at old messages without the user appearing online in the chat.

Messages can be listed in order of most recent to oldest and also in reverse (oldest to newest). Messages are returned in batches of up to 100 each with a cursor for when there are more messages.

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
let channelId = "<channel id>"

let result = try await client.listChannelMessages(session: session, channelId: channelId, limit: 10)

for message in result.messages {
    print("Message has id \(message.messageID) and content \(message.content)")
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const channelId = '<channel id>';
final result = await client.listChannelMessages(
  session: session,
  channelId: channelId,
  limit: 10,
);

for (final message in result.messages) {
  print('Message has id ${message.messageId} and content ${message.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
 6
 7
 8
 9
10
11
12
var channel_id = "<channel id>"
var result : NakamaAPI.ApiChannelMessageList = await client.list_channel_messages_async(session, channel_id, 10)

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>

A cursor can be used to page after a batch of messages for the next set of results.

We recommend you only list the most recent 100 messages in your UI. A good user experience could be to fetch the next 100 older messages when the user scrolls to the bottom of your UI panel.

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
let channelId = "<channel id>"

let result = try await client.listChannelMessages(session: session, channelId: channelId, limit: 10)

for message in result.messages {
    print("Message has id \(message.messageID) and content \(message.content)")
}

if !result.nextCursor.isEmpty {
    // Get the next 10 messages
    let result = try await client.listChannelMessages(session: session, channelId: channelId, limit: 10, cursor: result.nextCursor)

    for message in result.messages {
        print("Message has id \(message.messageID) and content \(message.content)")
    }
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const channelId = '<channel id>';
var result = await client.listChannelMessages(session: session, channelId: channelId, limit: 10);

for (final message in result.messages) {
  print('Message has id ${message.messageId} and content ${message.content}');
}

if (result.nextCursor.isNotEmpty) {
  // Get the next 10 messages.
  result = await client.listChannelMessages(
    session: session,
    channelId: channelId,
    limit: 10,
    cursor: result.nextCursor,
  );
  for (final message in result.messages) {
    print('Message has id ${message.messageId} and content ${message.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
 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 = await client.list_channel_messages_async(session, channel_id, 10, forward)

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 = await client.list_channel_messages_async(session, channel_id, 10, forward, result.next_cursor)

    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>

Cacheable cursors #

It can be useful to retrieve only the messages added since the list was last retrieved by a client. This can be done with the cacheable cursor returned with each channel message. Sending the cursor through a new list operation will retrieve only messages newer than those seen.

The cacheable cursor marks the position of the most recent channel messages retrieved. We recommend you store the cacheable cursor in device storage and use it when the client makes its next request for recent notifications.

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
let cacheableCursor = "<cacheableCursor>"
let channelId = "<channelId>"
var result = try await client.listChannelMessages(session: session, channelId: channelId, limit: 10, cacheableCursor: cacheableCursor)
for message in result.messages {
    print("Message has id \(message.messageId) and content \(message.content)")
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const cacheableCursor = '<cacheableCursor>';
const channelId = '<channelId>';
final result = await client.listChannelMessages(
  session: session,
  channelId: channelId,
  limit: 10,
  cacheableCursor: cacheableCursor,
);
for (final message in result.messages) {
  print('Message has id ${message.messageId} and content ${message.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])
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 = await client.list_channel_messages_async(session, channel_id, 10, forward, cacheable_cursor)
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])