From 3a664ea8e72af2fe4a1dabe9e4650ab743c1f1a8 Mon Sep 17 00:00:00 2001 From: Aimerny Date: Mon, 8 Jul 2024 21:11:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20add=20qq=20platform=20(#26)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 5 + readme-zh.md | 72 ++++++++++++++- readme.md | 71 ++++++++++++++- .../com/zhanganzhi/chathub/core/EventHub.java | 6 ++ .../chathub/core/config/Config.java | 20 ++++ .../chathub/platforms/Platform.java | 1 + .../chathub/platforms/qq/QQAdaptor.java | 91 +++++++++++++++++++ .../chathub/platforms/qq/QQFormatter.java | 16 ++++ .../chathub/platforms/qq/dto/QQEvent.java | 21 +++++ .../chathub/platforms/qq/dto/Sender.java | 11 +++ .../chathub/platforms/qq/protocol/QQAPI.java | 53 +++++++++++ .../platforms/qq/protocol/QQWsServer.java | 69 ++++++++++++++ src/main/resources/config.toml | 16 +++- 13 files changed, 449 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/zhanganzhi/chathub/platforms/qq/QQAdaptor.java create mode 100644 src/main/java/com/zhanganzhi/chathub/platforms/qq/QQFormatter.java create mode 100644 src/main/java/com/zhanganzhi/chathub/platforms/qq/dto/QQEvent.java create mode 100644 src/main/java/com/zhanganzhi/chathub/platforms/qq/dto/Sender.java create mode 100644 src/main/java/com/zhanganzhi/chathub/platforms/qq/protocol/QQAPI.java create mode 100644 src/main/java/com/zhanganzhi/chathub/platforms/qq/protocol/QQWsServer.java diff --git a/pom.xml b/pom.xml index 0c054df..add3cea 100644 --- a/pom.xml +++ b/pom.xml @@ -57,6 +57,11 @@ + + org.java-websocket + Java-WebSocket + 1.5.0 + diff --git a/readme-zh.md b/readme-zh.md index c85e85f..ec01380 100644 --- a/readme-zh.md +++ b/readme-zh.md @@ -4,6 +4,7 @@ [English](readme.md) [![License](https://shields.io/github/license/AnzhiZhang/ChatHub?label=License)](https://github.com/AnzhiZhang/ChatHub/blob/master/LICENSE) +[![Modrith](https://img.shields.io/modrinth/v/H3USaks7?logo=modrinth&label=Modrinth&color=%2300AF5C)](https://modrinth.com/plugin/chathub) [![CurseForge](https://cf.way2muchnoise.eu/short_825508_downloads.svg)](https://www.curseforge.com/minecraft/bukkit-plugins/chathub) [![Release](https://shields.io/github/v/release/AnzhiZhang/ChatHub?display_name=tag&include_prereleases&label=Release)](https://github.com/AnzhiZhang/ChatHub/releases/latest) [![Gitmoji](https://img.shields.io/badge/gitmoji-%20😜%20😍-FFDD67.svg)](https://gitmoji.dev/) @@ -259,10 +260,79 @@ Kook 消息的格式化文本。占位符与上文同理,所有的服务器名 ### qq -> 如需要此功能,欢迎 PR。 +> [!NOTE] +> `list` 指令暂未支持 + +该功能为双向转发,即 MC 内消息会发送到指定 QQ 群,QQ 群内消息将被转发到 MC 内。 #### enable 默认值:`false` 是否启用 [QQ](https://im.qq.com/index) 转发。 + +#### groupId + +QQ 群 ID。 + +### qq.api + +#### host + +Default: `0.0.0.0` + +OneBot 配置的反向 WebSocket 服务地址 + +#### wsReversePort + +Default: `9001` + +OneBot 配置的反向 WebSocket 服务端口 + +#### wsReversePath + +Default: `/ws/` + +websocket资源路径 + +> 这里是一个 OneBot 的反向 WebSocket 配置示例: `ws://127.0.0.1:9001/ws/` + +### qq.message + +QQ 消息的格式化文本。占位符与上文同理,所有的服务器名称会自动转为 plain 格式,您无需使用 plain 格式的占位符。 + +#### chat + +默认值:`[{server}] <{name}>: {message}` + +聊天消息。 + +#### join + +默认值:`[+] [{server}] {name}` + +玩家加入服务器消息。 + +#### leave + +默认值:`[-] {name}` + +玩家离开服务器消息。 + +#### switch + +默认值:`<{name}>: [{serverFrom}] ➟ [{serverTo}]` + +玩家切换服务器消息。 + +#### list + +默认值:`- [{server}] 当前共有{count}名玩家在线: {playerList}` + +`/list` 指令显示的消息。 + +#### listEmpty + +默认值:`当前没有玩家在线` + +使用 `/list` 指令且玩家列表为空时显示的消息。 diff --git a/readme.md b/readme.md index 9c88511..c924d06 100644 --- a/readme.md +++ b/readme.md @@ -260,10 +260,79 @@ Message for `/list` command when player list is empty. ### qq -> In case if you need this feature, PR is welcomed. +> [!NOTE] +> The `list` command is not currently available. + +The messages from the group will be synchronized to ChatHub and chatHub will forward all public messages to the group. #### enable Default: `false` Enable [QQ](https://im.qq.com/index) forwaring. + +#### groupId + +Group ID. + +### qq.api + +#### host + +Default: `0.0.0.0` + +OneBot server’s reverse webSocket host + +#### wsReversePort + +Default: `9001` + +OneBot server’s reverse webSocket port + +#### wsReversePath + +Default: `/ws/` + +Websocket resource location. + +> Here is a demo for one bot ws reverse path configuration: `ws://127.0.0.1:9001/ws/` + +### qq.message + +QQ message format sages. Placeholders are defined same as Miencraft, all server name will auto translate to plain format, you do not have to use plain placeholders. + +#### chat + +Default: `[{server}] <{name}>: {message}` + +Chat. + +#### join + +Default: `[+] [{server}] {name}` + +Message when player joined the server. + +#### leave + +Default: `[-] {name}` + +Message when player left the server. + +#### switch + +Default: `<{name}>: [{serverFrom}] ➟ [{serverTo}]` + +Message when player switched server. + +#### list + +Default: `- [{server}] 当前共有{count}名玩家在线: {playerList}` + +Message for `/list` command. + +#### listEmpty + +Default: `当前没有玩家在线` + +Message for `/list` command when player list is empty. diff --git a/src/main/java/com/zhanganzhi/chathub/core/EventHub.java b/src/main/java/com/zhanganzhi/chathub/core/EventHub.java index 08e2083..9b70807 100644 --- a/src/main/java/com/zhanganzhi/chathub/core/EventHub.java +++ b/src/main/java/com/zhanganzhi/chathub/core/EventHub.java @@ -9,6 +9,7 @@ import com.zhanganzhi.chathub.platforms.Platform; import com.zhanganzhi.chathub.platforms.discord.DiscordAdaptor; import com.zhanganzhi.chathub.platforms.kook.KookAdaptor; +import com.zhanganzhi.chathub.platforms.qq.QQAdaptor; import com.zhanganzhi.chathub.platforms.velocity.VelocityAdaptor; import java.util.ArrayList; @@ -34,6 +35,11 @@ public EventHub(ChatHub chatHub) { if (config.isKookEnabled()) { adaptors.add(new KookAdaptor(chatHub)); } + + // qq + if (config.isQQEnabled()) { + adaptors.add(new QQAdaptor(chatHub)); + } } public IAdaptor getAdaptor(Platform platform) { diff --git a/src/main/java/com/zhanganzhi/chathub/core/config/Config.java b/src/main/java/com/zhanganzhi/chathub/core/config/Config.java index 6523c26..b9f7c46 100644 --- a/src/main/java/com/zhanganzhi/chathub/core/config/Config.java +++ b/src/main/java/com/zhanganzhi/chathub/core/config/Config.java @@ -105,4 +105,24 @@ public Long getKookDaemonInterval() { public Long getKookDaemonRetry() { return configToml.getLong("kook.daemon.retry"); } + + public boolean isQQEnabled() { + return configToml.getBoolean("qq.enable"); + } + + public String getQQGroupId() { + return configToml.getString("qq.groupId"); + } + + public String getQQHost() { + return configToml.getString("qq.api.host"); + } + + public Long getQQWsReversePort() { + return configToml.getLong("qq.api.wsReversePort"); + } + + public String getQQWsReversePath() { + return configToml.getString("qq.api.wsReversePath", ""); + } } diff --git a/src/main/java/com/zhanganzhi/chathub/platforms/Platform.java b/src/main/java/com/zhanganzhi/chathub/platforms/Platform.java index 2a980c0..9820e51 100644 --- a/src/main/java/com/zhanganzhi/chathub/platforms/Platform.java +++ b/src/main/java/com/zhanganzhi/chathub/platforms/Platform.java @@ -6,6 +6,7 @@ public enum Platform { DISCORD("discord"), KOOK("kook"), + QQ("qq"), VELOCITY("velocity", "minecraft"); private final String name; diff --git a/src/main/java/com/zhanganzhi/chathub/platforms/qq/QQAdaptor.java b/src/main/java/com/zhanganzhi/chathub/platforms/qq/QQAdaptor.java new file mode 100644 index 0000000..a5e082e --- /dev/null +++ b/src/main/java/com/zhanganzhi/chathub/platforms/qq/QQAdaptor.java @@ -0,0 +1,91 @@ +package com.zhanganzhi.chathub.platforms.qq; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.zhanganzhi.chathub.ChatHub; +import com.zhanganzhi.chathub.core.adaptor.AbstractAdaptor; +import com.zhanganzhi.chathub.core.events.MessageEvent; +import com.zhanganzhi.chathub.platforms.Platform; +import com.zhanganzhi.chathub.platforms.qq.dto.QQEvent; +import com.zhanganzhi.chathub.platforms.qq.protocol.QQAPI; + +import java.util.ArrayList; +import java.util.List; + +public class QQAdaptor extends AbstractAdaptor { + private final QQAPI qqAPI; + private final Thread eventListener; + private boolean listenerStopFlag = false; + + public QQAdaptor(ChatHub chatHub) { + super(chatHub, Platform.QQ, new QQFormatter()); + qqAPI = new QQAPI(chatHub); + eventListener = new Thread(this::eventListener, "qq-event-listener"); + } + + @Override + public void start() { + chatHub.getLogger().info("QQ enabled"); + qqAPI.start(); + eventListener.start(); + } + + @Override + public void stop() { + // stop listener + listenerStopFlag = true; + + // interrupt listener, clear event queue + if (eventListener != null) { + eventListener.interrupt(); + } + + // close ws server + qqAPI.stop(); + } + + @Override + public void sendPublicMessage(String message) { + new Thread(() -> qqAPI.sendMessage(message, config.getQQGroupId())).start(); + } + + public void eventListener() { + while (!listenerStopFlag) { + consumeEvent(); + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + if (listenerStopFlag) { + // clear other event + consumeEvent(); + break; + } + } + } + } + + public void consumeEvent() { + QQEvent curEvent; + while ((curEvent = qqAPI.getQqEventQueue().poll()) != null) { + if ( + "message".equals(curEvent.getPostType()) + && "group".equals(curEvent.getMessageType()) + && "array".equals(curEvent.getMessageFormat()) + && config.getQQGroupId().equals(curEvent.getGroupId().toString()) + ) { + JSONArray message = curEvent.getMessage(); + List messages = new ArrayList<>(); + for (int i = 0; i < message.size(); i++) { + JSONObject part = message.getJSONObject(i); + if (part.getString("type").equals("text")) { + messages.add(part.getJSONObject("data").getString("text")); + } + } + String content = String.join(" ", messages); + chatHub.getEventHub().onUserChat(new MessageEvent( + Platform.QQ, null, curEvent.getSender().getNickname(), content + )); + } + } + } +} diff --git a/src/main/java/com/zhanganzhi/chathub/platforms/qq/QQFormatter.java b/src/main/java/com/zhanganzhi/chathub/platforms/qq/QQFormatter.java new file mode 100644 index 0000000..8033680 --- /dev/null +++ b/src/main/java/com/zhanganzhi/chathub/platforms/qq/QQFormatter.java @@ -0,0 +1,16 @@ +package com.zhanganzhi.chathub.platforms.qq; + +import com.zhanganzhi.chathub.core.formatter.AbstractFormatter; +import com.zhanganzhi.chathub.core.formatter.FormattingContent; +import com.zhanganzhi.chathub.platforms.Platform; + +public class QQFormatter extends AbstractFormatter { + protected QQFormatter() { + super(Platform.QQ); + } + + @Override + protected String replaceAll(String message, FormattingContent content) { + return getPlainString(super.replaceAll(message, content)); + } +} diff --git a/src/main/java/com/zhanganzhi/chathub/platforms/qq/dto/QQEvent.java b/src/main/java/com/zhanganzhi/chathub/platforms/qq/dto/QQEvent.java new file mode 100644 index 0000000..d6b6c9c --- /dev/null +++ b/src/main/java/com/zhanganzhi/chathub/platforms/qq/dto/QQEvent.java @@ -0,0 +1,21 @@ +package com.zhanganzhi.chathub.platforms.qq.dto; + +import com.alibaba.fastjson2.JSONArray; +import lombok.Data; + +@Data +public class QQEvent { + private Long selfId; + private Long messageId; + private Long realId; + private Long groupId; + private Long time; + private String postType; + private String metaEventType; + private String messageType; + private Sender sender; + private String rawMessage; + private String subType; + private JSONArray message; + private String messageFormat; +} diff --git a/src/main/java/com/zhanganzhi/chathub/platforms/qq/dto/Sender.java b/src/main/java/com/zhanganzhi/chathub/platforms/qq/dto/Sender.java new file mode 100644 index 0000000..88f0feb --- /dev/null +++ b/src/main/java/com/zhanganzhi/chathub/platforms/qq/dto/Sender.java @@ -0,0 +1,11 @@ +package com.zhanganzhi.chathub.platforms.qq.dto; + +import lombok.Data; + +@Data +public class Sender { + private Long userId; + private String nickname; + private String card; + private String role; +} diff --git a/src/main/java/com/zhanganzhi/chathub/platforms/qq/protocol/QQAPI.java b/src/main/java/com/zhanganzhi/chathub/platforms/qq/protocol/QQAPI.java new file mode 100644 index 0000000..f8619db --- /dev/null +++ b/src/main/java/com/zhanganzhi/chathub/platforms/qq/protocol/QQAPI.java @@ -0,0 +1,53 @@ +package com.zhanganzhi.chathub.platforms.qq.protocol; + +import com.alibaba.fastjson2.JSONObject; +import com.zhanganzhi.chathub.ChatHub; +import com.zhanganzhi.chathub.core.config.Config; +import com.zhanganzhi.chathub.platforms.qq.dto.QQEvent; +import lombok.Getter; +import lombok.SneakyThrows; +import org.slf4j.Logger; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedDeque; + +@Getter +public class QQAPI { + private final Config config = Config.getInstance(); + private final Queue qqEventQueue; + private final QQWsServer wsServer; + + public QQAPI(ChatHub chatHub) { + qqEventQueue = new ConcurrentLinkedDeque<>(); + wsServer = new QQWsServer( + config.getQQHost(), + config.getQQWsReversePort().intValue(), + config.getQQWsReversePath(), + qqEventQueue + ); + wsServer.setLogger(chatHub.getLogger()); + } + + public void start() { + wsServer.start(); + } + + @SneakyThrows + public void stop() { + wsServer.stop(); + } + + public void sendMessage(String message, String targetId) { + wsServer.sendMessage(genSendReq(message, targetId)); + } + + private String genSendReq(String message, String targetId) { + JSONObject req = new JSONObject(); + req.put("action", "send_msg"); + JSONObject params = new JSONObject(); + params.put("group_id", targetId); + params.put("message", message); + req.put("params", params); + return req.toJSONString(); + } +} diff --git a/src/main/java/com/zhanganzhi/chathub/platforms/qq/protocol/QQWsServer.java b/src/main/java/com/zhanganzhi/chathub/platforms/qq/protocol/QQWsServer.java new file mode 100644 index 0000000..990b047 --- /dev/null +++ b/src/main/java/com/zhanganzhi/chathub/platforms/qq/protocol/QQWsServer.java @@ -0,0 +1,69 @@ +package com.zhanganzhi.chathub.platforms.qq.protocol; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONReader; +import com.zhanganzhi.chathub.platforms.qq.dto.QQEvent; +import lombok.Setter; +import org.java_websocket.WebSocket; +import org.java_websocket.handshake.ClientHandshake; +import org.java_websocket.server.WebSocketServer; +import org.slf4j.Logger; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; + +public class QQWsServer extends WebSocketServer { + private final List clients; + private final String validResourcePath; + private final Queue qqEventDeque; + + @Setter + private Logger logger; + + public QQWsServer(String host, Integer port, String validResourcePath, Queue qqEventDeque) { + super(new InetSocketAddress(host, port)); + this.validResourcePath = validResourcePath; + this.qqEventDeque = qqEventDeque; + this.clients = new ArrayList<>(); + } + + @Override + public void onOpen(WebSocket webSocket, ClientHandshake clientHandshake) { + logger.info("QQ WebSocket server opened at [{}], path:[{}]", webSocket.getLocalSocketAddress(), clientHandshake.getResourceDescriptor()); + if (validResourcePath.equals(clientHandshake.getResourceDescriptor())) { + clients.add(webSocket); + } + } + + @Override + public void onClose(WebSocket webSocket, int i, String s, boolean b) { + logger.info("QQ WebSocket server closed"); + } + + @Override + public void onMessage(WebSocket webSocket, String msg) { + logger.debug("QQ WebSocket server received [{}]", msg); + QQEvent event = JSON.parseObject(msg, QQEvent.class, JSONReader.Feature.SupportSmartMatch); + logger.debug("parsed event:[{}]", event); + qqEventDeque.add(event); + } + + @Override + public void onError(WebSocket webSocket, Exception e) { + logger.error("QQ WebSocket server error", e); + } + + @Override + public void onStart() { + logger.info("QQ WebSocket server started"); + } + + public void sendMessage(String message) { + logger.debug("QQ WebSocket server send message to clients"); + for (WebSocket client : clients) { + client.send(message); + } + } +} diff --git a/src/main/resources/config.toml b/src/main/resources/config.toml index ca96321..31128fe 100644 --- a/src/main/resources/config.toml +++ b/src/main/resources/config.toml @@ -4,7 +4,7 @@ survival = '§2§l生存服' creative = '§6§l创造服' discord = '§a§lDiscord' kook = '§a§lKook' -qq = '§a§l群聊天' +qq = '§a§lQQ' [minecraft] completeTakeoverMode = false @@ -52,3 +52,17 @@ listEmpty = '当前没有玩家在线' [qq] enable = false +groupId = '' + +[qq.api] +host = '0.0.0.0' +wsReversePort = 9001 +wsReversePath = '/ws/' + +[qq.message] +chat = '[{server}] <{name}>: {message}' +join = '[+] [{server}] {name}' +leave = '[-] {name}' +switch = '<{name}>: [{serverFrom}] ➟ [{serverTo}]' +list = '- [{server}] 当前共有{count}名玩家在线: {playerList}' +listEmpty = '当前没有玩家在线'