# Real-time Chat

**URL:** https://heroiclabs.com/docs/nakama/concepts/chat/
**Summary:** Enable community interaction via direct, group, or public chat channels. Allows users to browse and join their desired chats, or create new ones. Messages in these channels can be persistent, enabling offline users to review them later, or visible to online users only.
**Keywords:** real-time chat, chat rooms, group chat, direct chat, private chat, persistence, message history, send chat message, leave chat
**Categories:** nakama, chat, concepts

---


# 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](../groups/). 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](../notifications/) 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](#join-chat) 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](#receive-messages).

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](../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(roomname, 1, 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" >}}
```swift
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)")
```
{{< / code >}}

{{< code type="client" >}}
```dart
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}'");
```
{{< / 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 >}}

{{< code type="client" framework="godot4" >}}
```gdscript
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])
```
{{< / code >}}

The `roomId` variable contains an ID used to [send messages](#send-messages).

### Groups

A group chat can only be joined by a user who is a member of the [group](../groups/). Messages are pushed in real-time to group members and they can read [historic messages](#message-history).
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](../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" >}}
```swift
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)")
```
{{< / code >}}

{{< code type="client" >}}
```dart
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}'");
```
{{< / 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 >}}

{{< code type="client" framework="godot4" >}}
```gdscript
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])
```
{{< / code >}}

The `"<group id>"` variable must be an ID used to [send messages](#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:

{{< 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" >}}
```swift
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)")
```
{{< / code >}}

{{< code type="client" >}}
```dart
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}'");
```
{{< / 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 >}}

{{< code type="client" framework="godot4" >}}
```gdscript
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])
```
{{< / code >}}

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

{{< 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" >}}
```swift
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)")
```
{{< / code >}}

{{< code type="client" >}}
```dart
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}'");
```
{{< / 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 >}}

{{< code type="client" framework="godot4" >}}
```gdscript
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])
```
{{< / code >}}

The `"<user id>"` variable must be an ID used to [send messages](#send-messages).

A user can [block other users](../friends/#block-a-friend) 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](#join-chat) 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.

{{< 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" >}}
```swift
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)
```
{{< / code >}}

{{< code type="client" >}}
```dart
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);
```
{{< /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 >}}

{{< code type="client" framework="godot4" >}}
```gdscript
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()])
```
{{< / code >}}

## 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.

{{< 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" >}}
```swift
socket.onChannelMessage = { message in
    print("Received a message on channel: \(message.channelID)")
    print("Message content: \(message.content)")
}
```
{{< / code >}}

{{< code type="client" >}}
```dart
socket.onChannelMessage.listen((message) {
  print('Received a message on channel: ${message.channelId}');
  print('Message content: ${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 >}}

{{< code type="client" framework="godot4" >}}
```gdscript
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])
```
{{< / code >}}

### 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`.

{{< 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" >}}
```swift
if message.code != 0 {
    print("Received message with code \(message.code)")
}
```
{{< / code >}}

{{< code type="client" >}}
```dart
if (message.code != 0) {
  print('Received message with code: ${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 >}}

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

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

## Send messages

When a user has [joined a chat channel](#join-chat), 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.

{{< 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" >}}
```swift
let channelId = "<channel id>"
let jsonContent = "\"message\": \"Hello world\""
let messageAck = try await socket.writeChatMessage(channelId: channelId, content: jsonContent)
```
{{< / code >}}

{{< code type="client" >}}
```dart
const channelId = '<channel id>';
final content = { 'message': 'Hello world' };
final sendAck = await socket.sendMessage(
  channelId: channelId,
  content: content,
);
print('Message sent: ${sendAck.messageId}');
```
{{< / 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 >}}

{{< code type="client" framework="godot4" >}}
```gdscript
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])
```
{{< / code >}}

### 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](../../server-framework/introduction/#hooks) in your [server runtime code](../../server-framework/) to either sanitize or reject any message containing inappropriate content.

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

{{< 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 >}}

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.

<!--
### 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 :)
-->

## 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.

{{< 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" >}}
```swift
let channelId = "<channel id>"
try await socket.leaveChat(channelId: channelId)
```
{{< / code >}}

{{< code type="client" >}}
```dart
const channelId = '<channel id>';
await socket.leaveChannel(channelId: 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 >}}

{{< code type="client" framework="godot4" >}}
```gdscript
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")
```
{{< / code >}}

## Message history

Every chat conversation stores a history of messages (unless persistence is set false). The history also contains [event messages](#receive-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.

{{< 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" >}}
```swift
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)")
}
```
{{< / code >}}

{{< code type="client" >}}
```dart
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}');
}
```
{{< / 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" framework="godot4" >}}
```gdscript
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])
```
{{< / 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 >}}

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.

{{< 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" >}}
```swift
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)")
    }
}
```
{{< / code >}}

{{< code type="client" >}}
```dart
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}');
  }
}
```
{{< / 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" framework="godot4" >}}
```gdscript
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])
```
{{< / 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 >}}

### 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.

{{< 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" >}}
```swift
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)")
}
```
{{< / code >}}

{{< code type="client" >}}
```dart
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}');
}
```
{{< / 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 >}}

{{< code type="client" framework="godot4" >}}
```gdscript
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])
```
{{< / code >}}
