From 5f93fa2ec605435e48731a16d53a877ffed79616 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=82=85=E4=B8=9C=E6=B5=B7?= <296322762@qq.com>
Date: Thu, 21 Dec 2023 16:05:45 +0800
Subject: [PATCH] Add chatroom directed message function

---
 .../im/server/api/message/MessageIT.java      | 75 +++++++++++++++++++
 .../im/server/api/message/MessageApi.java     | 49 +++++++++++-
 .../api/message/send/message/MessageSend.java | 50 +++++++++++++
 .../send/message/MessageSendRequest.java      | 19 +++++
 4 files changed, 192 insertions(+), 1 deletion(-)

diff --git a/im-sdk-core/src/integration-test/java/com/easemob/im/server/api/message/MessageIT.java b/im-sdk-core/src/integration-test/java/com/easemob/im/server/api/message/MessageIT.java
index fba9c7103..aad33c249 100644
--- a/im-sdk-core/src/integration-test/java/com/easemob/im/server/api/message/MessageIT.java
+++ b/im-sdk-core/src/integration-test/java/com/easemob/im/server/api/message/MessageIT.java
@@ -985,4 +985,79 @@ void testGroupMessageSendMsgTextSyncDevice() {
                 () -> this.service.group().destroyGroup(groupId).block(Utilities.IT_TIMEOUT));
     }
 
+    @Test
+    void testDirectionalChatroomMessageSendText() {
+        String randomFromUsername = Utilities.randomUserName();
+        String randomPassword = Utilities.randomPassword();
+
+        String randomToUsername = Utilities.randomUserName();
+        assertDoesNotThrow(() -> this.service.user().create(randomFromUsername, randomPassword)
+                .block(Utilities.IT_TIMEOUT));
+        assertDoesNotThrow(() -> this.service.user().create(randomToUsername, randomPassword)
+                .block(Utilities.IT_TIMEOUT));
+
+        List<String> members = new ArrayList<>();
+        members.add(randomToUsername);
+        String roomId = assertDoesNotThrow(() -> this.service.room()
+                .createRoom("chat room", "room description", randomFromUsername, members, 200)
+                .block(Utilities.IT_TIMEOUT));
+
+        Set<String> roomIds = new HashSet<>();
+        roomIds.add(roomId);
+
+        assertDoesNotThrow(() -> {
+            EMSentMessageIds sentMessageIds = this.service.message().sendMsg()
+                    .fromUser(randomFromUsername)
+                    .toRooms(roomIds)
+                    .text(msg -> msg.text("hello"))
+                    .toRoomUsers(new HashSet<String>() {
+                        {
+                            add(randomToUsername);
+                        }
+                    })
+                    .extension(exts -> exts.add(EMKeyValue.of("timeout", 1)))
+                    .chatroomMsgLevel(ChatroomMsgLevel.NORMAL)
+                    .syncDevice(true)
+                    .send()
+                    .block(Utilities.IT_TIMEOUT);
+
+            assertEquals(roomId, sentMessageIds.getMessageIdsByEntityId().keySet().iterator().next());
+        });
+
+        assertDoesNotThrow(() -> {
+            Set<String> toChatRooms = new HashSet<>();
+            toChatRooms.add(roomId);
+
+            Set<String> toRoomUsers = new HashSet<>();
+            toRoomUsers.add(randomToUsername);
+
+            EMImageMessage imageMessage =
+                    new EMImageMessage().uri(URI.create("http://example/image.png"))
+                            .secret("secret")
+                            .displayName("image.png");
+
+            Set<EMKeyValue> exts = new HashSet<>();
+            exts.add(EMKeyValue.of("key", "value"));
+            exts.add(EMKeyValue.of("key1", 10));
+            exts.add(EMKeyValue.of("key2", new HashMap<String, String>() {
+                {
+                    put("mkey1", "mvalue1");
+                    put("mkey2", "mvalue2");
+                }
+            }));
+
+            EMSentMessageIds messageIds = service.message()
+                    .sendMsg(randomFromUsername, toChatRooms, imageMessage, toRoomUsers, exts, true,
+                            ChatroomMsgLevel.HIGH).block();
+            assertEquals(roomId, messageIds.getMessageIdsByEntityId().keySet().iterator().next());
+        });
+
+        assertDoesNotThrow(
+                () -> this.service.room().destroyRoom(roomId).block(Utilities.IT_TIMEOUT));
+        assertDoesNotThrow(
+                () -> this.service.user().delete(randomFromUsername).block(Utilities.IT_TIMEOUT));
+        assertDoesNotThrow(
+                () -> this.service.user().delete(randomToUsername).block(Utilities.IT_TIMEOUT));
+    }
+
 }
diff --git a/im-sdk-core/src/main/java/com/easemob/im/server/api/message/MessageApi.java b/im-sdk-core/src/main/java/com/easemob/im/server/api/message/MessageApi.java
index 5931aa9c9..5c0216aa6 100644
--- a/im-sdk-core/src/main/java/com/easemob/im/server/api/message/MessageApi.java
+++ b/im-sdk-core/src/main/java/com/easemob/im/server/api/message/MessageApi.java
@@ -586,7 +586,7 @@ public Mono<EMSentMessageIds> sendMsg(String from, String toType, Set<String> to
      * }</pre>
      *
      * @param from         发送者用户名
-     * @param toGroups          消息接收方所属的群组 ID。目前每次只能传 1 个群组。
+     * @param toGroups     消息接收方所属的群组 ID。目前每次只能传 1 个群组 ID。
      * @param message      要发送的消息
      * @param toGroupUsers 接收消息的群成员的用户 ID 数组。每次最多可传 20 个用户 ID。
      * @param extensions   要发送的扩展,可以为空
@@ -601,6 +601,53 @@ public Mono<EMSentMessageIds> sendMsg(String from, Set<String> toGroups,
                 syncDevice);
     }
 
+    /**
+     * 指定聊天室用户发送消息,你可以向聊天室中指定的一个或多个成员发送消息,但单次仅支持指定一个聊天室。
+     * 对于定向消息,只有作为接收方的指定成员才能看到消息,其他聊天室成员则看不到该消息。
+     *
+     * <p>
+     * API使用示例:
+     * <pre> {@code
+     * EMService service;
+     *
+     * 例如,向指定聊天室用户发送一条带有扩展字段的文本消息
+     * Set<String> toRooms = new HashSet<>();
+     * toRooms.add("toRoomId");
+     *
+     * Set<String> toRoomUsers = new HashSet<>();
+     * toRoomUsers.add("toUserName");
+     *
+     * EMTextMessage textMessage = new EMTextMessage().text("hello");
+     *
+     * Set<EMKeyValue> exts = new HashSet<>();
+     * exts.add(EMKeyValue.of("key", "value"));
+     *
+     * try {
+     *     EMSentMessageIds messageIds = service.message().sendMsg("fromUserName", toRooms, textMessage, toRoomUsers, exts, true, ChatroomMsgLevel.NORMAL).block();
+     * } catch (EMException e) {
+     *     e.getErrorCode();
+     *     e.getMessage();
+     * }
+     *
+     * }</pre>
+     *
+     * @param from             发送者用户名
+     * @param toRooms          消息接收方所属的聊天室 ID。目前每次只能传 1 个聊天室 ID。
+     * @param message          要发送的消息
+     * @param toRoomUsers      接收消息的聊天室成员的用户 ID 数组。每次最多可传 20 个用户 ID。
+     * @param extensions       要发送的扩展,可以为空
+     * @param syncDevice       消息发送成功后,是否将消息同步到发送方,true:是同步给发送方,false:是不同给发送方
+     * @param chatroomMsgLevel 聊天室消息优先级: LOW-低优先级,NORMAL-普通优先级,HIGH-高优先级
+     * @return 发消息响应或错误
+     * @see <a href="http://docs-im-beta.easemob.com/document/server-side/message_chatroom.html#%E5%8F%91%E9%80%81%E5%AE%9A%E5%90%91%E6%B6%88%E6%81%AF">发送定向消息</a>
+     */
+    public Mono<EMSentMessageIds> sendMsg(String from, Set<String> toRooms,
+            EMMessage message, Set<String> toRoomUsers, Set<EMKeyValue> extensions,
+            Boolean syncDevice, ChatroomMsgLevel chatroomMsgLevel) {
+        return this.messageSend.send(from, checkTos(toRooms), message, checkTos(toRoomUsers), extensions,
+                syncDevice, chatroomMsgLevel);
+    }
+
     /**
      * 发送消息(只投递在线消息),不返回消息 ID。将在后续版本中移除。
      * <p>
diff --git a/im-sdk-core/src/main/java/com/easemob/im/server/api/message/send/message/MessageSend.java b/im-sdk-core/src/main/java/com/easemob/im/server/api/message/send/message/MessageSend.java
index 1d4da658b..ec4e6a815 100644
--- a/im-sdk-core/src/main/java/com/easemob/im/server/api/message/send/message/MessageSend.java
+++ b/im-sdk-core/src/main/java/com/easemob/im/server/api/message/send/message/MessageSend.java
@@ -271,6 +271,34 @@ public Mono<EMSentMessageIds> send(String from, Set<String> tos, EMMessage messa
                 });
     }
 
+    public Mono<EMSentMessageIds> send(String from, Set<String> tos, EMMessage message, Set<String> toRoomUsers,
+            Set<EMKeyValue> extensions, Boolean syncDevice, ChatroomMsgLevel chatroomMsgLevel) {
+        return this.context.getHttpClient()
+                .flatMap(httpClient -> httpClient.post()
+                        .uri("/messages/chatrooms/users")
+                        .send(Mono.create(sink -> {
+                            sink.success(context.getCodec()
+                                    .encode(new MessageSendRequest(from, tos, message, toRoomUsers,
+                                            MessageSendRequest.parseExtensions(extensions),
+                                            syncDevice, chatroomMsgLevel)));
+                        }))
+                        .responseSingle((rsp, buf) -> {
+                            return buf.switchIfEmpty(
+                                            Mono.error(new EMUnknownException("response is null")))
+                                    .flatMap(byteBuf -> {
+                                        ErrorMapper mapper = new DefaultErrorMapper();
+                                        mapper.statusCode(rsp);
+                                        mapper.checkError(byteBuf);
+                                        return Mono.just(byteBuf);
+                                    });
+                        }))
+                .map(byteBuf -> {
+                    SendMessageResponse sendMessageResponse = context.getCodec()
+                            .decode(byteBuf, SendMessageResponse.class);
+                    return sendMessageResponse.toEMSentMessages();
+                });
+    }
+
     public class RouteSpec {
 
         private String from;
@@ -389,6 +417,8 @@ public class SendSpec {
 
         private Set<String> toGroupUsers;
 
+        private Set<String> toRoomUsers;
+
         private Set<EMKeyValue> extensions;
 
         private String routeType;
@@ -452,6 +482,17 @@ public class SendSpec {
             this.syncDevice = syncDevice;
         }
 
+        SendSpec(String from, String toType, Set<String> tos, EMMessage message, Set<String> toRoomUsers,
+                Boolean syncDevice, ChatroomMsgLevel chatroomMsgLevel) {
+            this.from = from;
+            this.toType = toType;
+            this.tos = tos;
+            this.message = message;
+            this.toRoomUsers = toRoomUsers;
+            this.syncDevice = syncDevice;
+            this.chatroomMsgLevel = chatroomMsgLevel;
+        }
+
         public MessageSend.SendSpec extension(Consumer<Set<EMKeyValue>> customizer) {
             if (this.extensions == null) {
                 this.extensions = new HashSet<>();
@@ -480,6 +521,11 @@ public MessageSend.SendSpec toGroupUsers(Set<String> toGroupUsers) {
             return this;
         }
 
+        public MessageSend.SendSpec toRoomUsers(Set<String> toRoomUsers) {
+            this.toRoomUsers = toRoomUsers;
+            return this;
+        }
+
         public Mono<EMSentMessageIds> send() {
             if (CHAT_ROOMS.equalsIgnoreCase(this.toType)) {
                 if (StringUtils.isNotEmpty(routeType) && syncDevice != null) {
@@ -491,6 +537,10 @@ public Mono<EMSentMessageIds> send() {
                             .send(this.from, this.toType, this.tos, this.message, this.extensions,
                                     this.routeType, this.chatroomMsgLevel);
                 } else if (syncDevice != null) {
+                    if (toRoomUsers != null) {
+                        return MessageSend.this.send(this.from, this.tos, this.message,
+                                this.toRoomUsers, this.extensions, this.syncDevice, this.chatroomMsgLevel);
+                    }
                     return MessageSend.this
                             .send(this.from, this.toType, this.tos, this.message, this.extensions,
                                     this.syncDevice, this.chatroomMsgLevel);
diff --git a/im-sdk-core/src/main/java/com/easemob/im/server/api/message/send/message/MessageSendRequest.java b/im-sdk-core/src/main/java/com/easemob/im/server/api/message/send/message/MessageSendRequest.java
index c86a4cc5a..0157bef4c 100644
--- a/im-sdk-core/src/main/java/com/easemob/im/server/api/message/send/message/MessageSendRequest.java
+++ b/im-sdk-core/src/main/java/com/easemob/im/server/api/message/send/message/MessageSendRequest.java
@@ -188,6 +188,25 @@ public MessageSendRequest(@JsonProperty("from") String from,
         this.syncDevice = syncDevice;
     }
 
+    @JsonCreator
+    public MessageSendRequest(@JsonProperty("from") String from,
+            @JsonProperty("to") Set<String> toSet,
+            @JsonProperty("msg") EMMessage message,
+            @JsonProperty("users") Set<String> toRoomUsers,
+            @JsonProperty("ext") Map<String, Object> extensions,
+            @JsonProperty("sync_device") Boolean syncDevice,
+            @JsonProperty("chatroom_msg_level") ChatroomMsgLevel chatroomMsgLevel) {
+        this.from = from;
+        this.tos = toSet;
+        this.toGroupUsers = toRoomUsers;
+        Message send = Message.of(message);
+        this.msgType = send.type;
+        this.body = send;
+        this.extensions = extensions;
+        this.syncDevice = syncDevice;
+        this.chatroomMsgLevel = chatroomMsgLevel.getLevel();
+    }
+
     public static Map<String, Object> parseExtensions(Set<EMKeyValue> extensions) {
         if (extensions == null || extensions.isEmpty()) {
             return null;