# 실시간 채팅

**URL:** https://heroiclabs.com/docs/kr/nakama/concepts/chat/
**Summary:** 직접, 그룹 또는 공개 채팅 채널을 통해 커뮤니티 상호 작용을 활성화합니다. 사용자는 원하는 채팅을 찾아 가입하거나 새 채팅을 만들 수 있습니다. 이 채널의 메시지가 영구적이 되어 오프라인 사용자가 나중에 검토할 수 있거나, 온라인 사용자에게만 표시될 수 있습니다.

---


# 실시간 채팅

실시간 채팅을 통해 라이브 커뮤니티가 쉽게 활성화됩니다.

사용자는 1대1로 또는 그룹의 일부로 또는 채팅방에서 채팅할 수 있습니다. 메시지에는 이미지, 링크 및 기타 컨텐츠를 포함시킬 수 있습니다. 수신자가 온라인 상태면 이러한 메시지는 즉시 클라이언트에게 전달되고 메시지 기록에 저장되므로 오프라인 사용자가 온라인 상태가 되면 쉽게 확인할 수 있습니다.

실시간 채팅 엔진을 통해 전달되는 모든 메시지는 메시지를 받을 사용자를 내부적으로 식별하기 위해 사용되는 채널에 속합니다. 사용자는 연결될 때 채널을 명시적으로 가입하고 탈퇴합니다. 이렇게 하면 관심있는 메시지를 선택적으로 듣거나 또는 사용 중인 특정 채널을 쉽게 "음소거"할 수 있습니다. 사용자는 한 번에 여러 채널에 가입하여 여러 그룹이나 채팅방에서 동시에 채팅할 수도 있습니다.

## 채팅 채널

채널에는 3 가지 유형이 있습니다:

1. 채팅방은 공개적인 채팅에 좋습니다. 어떤 사용자도 허가없이 가입하고 참여할 수 있습니다. 이런 채팅방은 동시 커뮤니케이션에서 수백만 명의 사용자로 확장될 수 있습니다. 라이브 참여 앱 또는 라이브 이벤트 또는 토너먼트가 있는 게임에 적합합니다.

2. 그룹 채팅은 [그룹](../groups/)의 사용자에게만 공개됩니다. 각 사용자는 그룹의 구성원이어야 하며 다른 사용자는 참여할 수 없습니다. 팀 기반 게임 플레이 또는 협업에서 그룹 채팅을 사용할 수 있습니다.

3. 직접 채팅은 두 사용자에게만 공개됩니다. 각 사용자는 채팅에 초대되면 [알림](../notifications/)이 전달됩니다. 나쁜 사용자의 스팸을 방지하기 위해 메시지를 교환하려면 두 사용자 모두 가입해야 합니다.

### 지속성

기본적으로 모든 채널은 영구적이므로 채널을 통해 주고 받은 메시지는 데이터베이스에 저장되며 나중에 메시지 기록에서 사용할 수 있습니다. 이 기록을 통해 오프라인 사용자는 확인하지 못한 메시지를 다음에 연결한 후 확인할 수 있습니다.

메시지를 온라인 사용자에게만 보내고 메시지 기록에 보관하지 않기 위해 클라이언트는 지속성이 비활성화된 [채널에 가입](#join-chat)할 수 있습니다.

### 숨겨진 채널 구성원

기본적으로 채널에 가입하는 모든 사용자는 다른 사용자에게 표시됩니다. 기존 채널 참가자는 사용자가 연결 및 연결 해제될 때 이벤트를 받으며 새로운 채널 가입자는 이미 채널에 있는 사용자 목록을 받습니다.

사용자는 연결할 때 자신의 채널 존재를 숨기도록 선택할 수 있으므로 사용자의 가입/탈퇴 알림이 생성되지 않고 사용자가 채널 구성원 목록에 표시되지 않습니다. 이런 경우에도 사용자는 평소처럼 실시간 메시지를 주고 받을 수 있습니다.

## 채팅 가입

다른 사용자에게 메시지를 보내려면 사용자는 통신할 채팅 채널에 가입해야 합니다. 이렇게 하면 [실시간으로 메시지를 받을](#receive-messages) 수도 있습니다.

각 사용자는 자신의 세션으로 다양한 방, 그룹 및 직접 채팅에 가입할 수 있습니다. 각 장치는 별도의 세션으로 식별되므로 한 사용자가 다른 장치에서 같은 채팅에 연결할 수도 있습니다.

### 채팅방

채팅방은 사용자가 채팅할 수 있게 동적으로 생성됩니다. 채팅방에는 이름이 있으며 사용자가 가입할 때 서버에 설정됩니다. 가입할 수 있는 채팅방 이름 목록은 클라이언트 코드에 저장하거나 [저장소 기록](../storage/collections/)으로 원격 구성을 통해 저장할 수 있습니다.

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

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

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

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

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

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

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

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

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

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

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

`roomId` 변수에는 [메시지를 보내는 데](#send-messages) 사용되는 ID가 포함됩니다.

### 그룹

그룹 채팅은 [그룹](../groups/)의 구성원인 사용자만 가입할 수 있습니다. 메시지는 실시간으로 그룹 구성원에게 푸시되며 구성원들은 [기록 메시지](#message-history)를 읽을 수 있습니다.
쫓겨나거나 그룹을 나간 사용자는 더 이상 메시지를 받거나 기록을 읽을 수 없습니다.

그룹 채팅에 가입하는 사용자는 그룹 ID가 필요하며 [사용자는 이를 나열](../groups/#list-groups)할 수 있습니다.

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

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

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

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

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

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

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

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

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

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

`"<group id>"` 변수는 [메시지를 보내는데](#send-messages) 사용되는 ID여야 합니다.

### 직접

사용자는 ID를 통해 다른 사용자에게 메시지를 직접 보낼 수 있습니다. 친구, 그룹, 순위표, 매치메이커, 채팅방 및 저장소 검색은 모두 채팅할 사용자를 찾는 방법입니다.

두 사용자가 모두 채팅에 가입할 때까지 실시간으로 메시지가 전달되지 않습니다. 이렇게 하면 나쁜 사용자가 보내는 스팸 메시지가 방지되므로 중요합니다.

다른 사용자를 직접 채팅에 초대하려면:

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

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

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

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

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

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

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

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

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

채팅 요청이 들어오면 사용자는 [인앱 알림](../notifications/)을 받게 됩니다. 초대를 수락하려면 사용자는 요청하는 사용자를 대상으로 동일한 `joinChat` 요청으로 응답합니다.

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

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

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

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

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

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

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

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

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

`"<user id>"` 변수는 [메시지를 보내는데](#send-messages) 사용되는 ID여야 합니다.

사용자는 원치 않는 직접 메시지를 중지하기 위해 [다른 사용자를 차단](../friends/#block-a-friend)할 수 있습니다.

## 온라인 사용자 나열

채팅에 가입하는 각 사용자는 채팅 채널에서 "현재 상태"가 됩니다("숨겨진" 채널 사용자로 참여하지 않은 경우). 이러한 현재 상태를 통해 온라인 상태인 사용자를 확인할 수 있습니다.

현재 상태는 사용자 ID와 결합된 고유한 세션으로 구성됩니다. 이렇게 하면 채팅 채널에서 여러 장치를 사용하여 연결한 사용자를 쉽게 구별할 수 있습니다.

[채팅 채널에 가입](#join-chat)하는 사용자에게는 채팅 채널에서 연결된 다른 모든 사용자의 초기 현재 상태 목록이 제공됩니다. 콜백을 사용하여 가입 및 탈퇴하는 사용자의 현재 상태 변경 사항을 서버에서 받을 수 있습니다. 이렇게 하면 온라인 사용자 목록을 쉽게 유지 관리하고 변경 사항이 있을 때 업데이트할 수 있습니다.

서버는 다른 사용자가 채팅에 가입하거나 탈퇴할 때만 현재 상태 업데이트를 푸시하도록 최적화되어 있습니다.

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

{{< 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 response = await socket.joinChat(roomname, 1, persistence, hidden);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

## 메시지 받기

사용자가 채팅 채널에 가입하여 실시간으로 메시지를 수신하기 시작합니다. 새 메시지는 각각 이벤트 핸들러에서 수신하고 UI에 추가할 수 있습니다. 메시지는 서버에서 처리한 순서대로 전달됩니다.

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

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

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

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

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

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

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

### 그룹

그룹 채팅의 경우 사용자는 서버로부터 다른 메시지를 받습니다. 이 메시지에는 그룹에 가입하거나 탈퇴하는 사용자에 대한 이벤트, 누군가가 관리자로 승격되는 경우 등이 포함됩니다. 사용자가 채팅 스트림에서 이러한 메시지를 보거나 UI에서 무시하도록 지정할 수 있습니다.

`Type` 메시지로 채팅 메시지와 이벤트 메시지를 식별할 수 있습니다.

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

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

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

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

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

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

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

## 메시지 보내기

사용자가 [채팅 채널에 가입](#join-chat)하면 해당 ID를 사용하여 JSON 인코딩 문자열로 메시지를 보낼 수 있습니다.

전송된 모든 메시지는 서버에서 수신할 때 승인을 반환합니다. 반환된 승인에는 메시지 ID, 타임스탬프 및 메시지를 보낸 사용자에 대한 세부 정보가 포함됩니다.

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

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

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

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

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

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

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

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

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

### 메시지 컨텐츠 필터링

모욕적이거나 공격적인 행위를 방지하기 위해 채팅 채널에서 사용자의 메시지를 필터링하는 것이 일반적입니다. 이는 [서버 런타임 코드](../../server-framework/)에 [후크](../../server-framework/introduction/#hooks)를 사용하여 부적절한 컨텐츠가 포함된 메시지를 삭제하거나 거부함으로써 Nakama에서 달성할 수 있습니다.

이 코드는 메시지가 전송될 때 서버에서 트리거됩니다.

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

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

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

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

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

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

    return e;
}

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

위 예의 경우, 고유한 필터링 기준("악성 단어" 인덱스)을 만들고 유지 관리하면서 원하는 경우 타사 필터링 API와의 통합도 선택할 수 있습니다.

<!--
### Shadow banning

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

// Code samples by Tom :)
-->

## 채팅 나가기

사용자는 더 이상 실시간으로 메시지를 보내지 않기 위해 채팅 채널을 나갈 수 있습니다. 이는 UI의 다른 부분에 있는 동안 채팅을 "음소거"하는데 유용할 수 있습니다.

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

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

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

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

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

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

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

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

## 메시지 기록

모든 채팅 대화에서 메시지 기록은 저장됩니다(지속성이 false로 설정되지 않은 경우). 이 기록에는 서버가 그룹 채팅 채널에 보낸 [이벤트 메시지](#receive-messages)도 포함됩니다. 각 사용자는 다음에 온라인으로 연결할 때 채널의 이전 메시지를 검색할 수 있습니다.
사용자는 채팅 기록을 보기 위해 채팅 채널에 가입할 필요가 없습니다. 이는 사용자가 채팅에 온라인으로 표시되지 않은 상태에서 메시지를 "미리 보기"하는데 유용합니다.

메시지는 최신 순 또는 역순(오래된 것에서 가장 최근 것)으로 나열될 수도 있습니다. 메시지는 더 많은 메시지가 있는 경우 커서를 포함하여 각각 최대 100개의 배치로 반환됩니다.

{{< missing type="client" lang="lua" framework="defold" />}}


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

커서를 사용하여 다음 결과 세트를 위해 메시지 배치 처리 후 페이징할 수 있습니다.

UI에는 최근 100개의 메시지만 나열하는 것이 좋습니다. 좋은 사용자 경험은 사용자가 UI 패널의 맨 아래로 스크롤할 때 다음 100개의 오래된 메시지를 표시하는 것입니다.

{{< missing type="client" lang="lua" framework="defold" />}}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

### 캐시 가능한 커서

클라이언트가 목록을 마지막으로 검색한 이후에 추가된 메시지만 검색하는 것이 더 좋을 수 있습니다. 이는 각 채널 메시지와 함께 반환된 캐시 가능한 커서로 수행할 수 있습니다. 새 목록 작업을 통해 커서를 보내면 표시된 메시지보다 더 최신인 메시지만 검색합니다.

캐시 가능한 커서는 검색된 가장 최근 채널 메시지의 위치를 표시합니다. 캐시 가능한 커서를 장치 저장소에 저장하고 클라이언트가 최근 알림에 대한 다음 요청을 실시할 때 사용하는 것이 좋습니다.

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

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

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

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

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

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

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