mirror of
https://github.com/jie65535/gc-openchat-plugin.git
synced 2025-06-01 17:29:11 +08:00
Implement OneBot protocol
Add subcommand `group` Update version to v0.2.0 Update plugin config
This commit is contained in:
parent
d060b1804f
commit
d8caa42899
@ -1,6 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<file type="web" url="file://$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
|
111
README.md
111
README.md
@ -18,9 +18,10 @@
|
||||
- [x] 玩家间聊天
|
||||
- [x] 聊天管理命令
|
||||
- [x] 发言频率限制
|
||||
- [x] 聊天api (OneBot)
|
||||
- [x] 玩家上下线提醒
|
||||
- [ ] 指定管理账号在群内执行控制台命令
|
||||
- [ ] 聊天内容审查
|
||||
- [ ] 聊天api _(~~OneBot api~~)_
|
||||
- [ ] ...
|
||||
|
||||
## 安装
|
||||
|
||||
@ -39,37 +40,125 @@
|
||||
- `/serverchat unban @uid` 解除指定玩家禁言
|
||||
- `/serverchat limit <次每分钟>` 设置发消息频率限制
|
||||
- `/serverchat reload` 重载配置文件
|
||||
- `/serverchat group <groupId>` 设置互联群号
|
||||
|
||||
`/serverchat` 可用别名 `/sc`
|
||||
`/serverchat` 可用别名 `/sc`,例如 `/sc ban @xxx`
|
||||
|
||||
## 群服互联
|
||||
|
||||
## 配置
|
||||
推荐使用 [go-cqhttp](https://github.com/Mrs4s/go-cqhttp) ,[快速开始](https://docs.go-cqhttp.org/guide/quick_start.htm)
|
||||
|
||||

|
||||
|
||||
除了登录设置外,需[配置](https://docs.go-cqhttp.org/guide/config.html) `config.yml` 中以下内容
|
||||
- `access-token: ''` 为插件配置中的token
|
||||
- `ws-reverse:`
|
||||
- `universal: ws://your_websocket_universal.server` 为OpenChat地址,例如 `ws://127.0.0.1:443/openchat`
|
||||
|
||||
建议使用 `Android Watch` 协议登录(在 `device.json` 中 `"protocol": 5` 修改为 `"protocol": 2` )
|
||||
|
||||
---
|
||||
|
||||
## 插件配置
|
||||
```json5
|
||||
{
|
||||
// 服务器聊天开关
|
||||
serverChatEnabled: true,
|
||||
"serverChatEnabled": true,
|
||||
|
||||
// 服务器聊天消息格式
|
||||
// {nickName} 为玩家昵称
|
||||
// {uid} 为玩家UID
|
||||
// {message} 为消息内容
|
||||
serverChatFormat: "<color=#99CC99>{nickName}({uid})</color>: {message}",
|
||||
"serverChatFormat": "<color=#99CC99>{nickName}({uid})</color>: {message}",
|
||||
|
||||
// 每分钟发言消息数限制
|
||||
messageFreLimitPerMinute: 20,
|
||||
"messageFreLimitPerMinute": 20,
|
||||
|
||||
// 是否在玩家加入时发送消息
|
||||
sendJoinMessage: true,
|
||||
"sendJoinMessage": true,
|
||||
|
||||
// 玩家加入时发送消息
|
||||
joinMessage: "本服已启用聊天,/chat on 开启(默认),/chat off 屏蔽",
|
||||
"joinMessage": "本服已启用聊天,/chat on 开启(默认),/chat off 屏蔽",
|
||||
|
||||
// 被禁言反馈消息
|
||||
bannedFeedback: "你已经被禁言!",
|
||||
"bannedFeedback": "你已经被禁言!",
|
||||
|
||||
// 消息太频繁反馈消息
|
||||
// {limit} 服务器设置的限制次数
|
||||
msgTooFrequentFeedback: "服务器设置每分钟仅允许发言{limit}次"
|
||||
"msgTooFrequentFeedback": "服务器设置每分钟仅允许发言{limit}次",
|
||||
|
||||
// 是否将聊天log
|
||||
"logChat": true,
|
||||
|
||||
// WebSocket Access Token
|
||||
// 安全令牌,仅允许授权的连接
|
||||
// 如果为空将会在启动时自动生成一个32位随机令牌并显示在控制台
|
||||
"wsToken": "",
|
||||
|
||||
// WebSocket Path
|
||||
// 反向WS的路径,即机器人连接到本插件开放的WS接口路径
|
||||
// 若不想开放WS,则留空,默认为 /openchat
|
||||
// OneBot设置示例:ws://127.0.0.1:443/openchat
|
||||
"wsPath": "/openchat",
|
||||
|
||||
// // WebSocket Address
|
||||
// // 正向WS的地址,即本插件主动连接机器人开放的WS接口地址
|
||||
// // 示例:ws://127.0.0.1:8080
|
||||
// // 若不需要,则留空
|
||||
// // TODO:由于需要引入外部依赖,正向WS方式暂不实现
|
||||
// public String wsAddress: "",
|
||||
|
||||
// 群ID
|
||||
// 可以使用指令 `/sc group <groupId>` 设定
|
||||
"groupId": 0,
|
||||
|
||||
// 群消息格式化
|
||||
// {id} 为QQ号
|
||||
// {name} 为群名片,如果为空则显示昵称
|
||||
// {message} 为消息
|
||||
"groupToGameFormat": "<color=#6699CC>[QQ]</color><color=#99CC99>{name}</color>: {message}",
|
||||
|
||||
// 服务器聊天消息格式
|
||||
// {nickName} 为玩家昵称
|
||||
// {uid} 为玩家UID
|
||||
// {message} 为消息内容
|
||||
"gameToGroupFormat": "[GC]{nickName}({uid}): {message}",
|
||||
|
||||
// /**
|
||||
// * 频道ID
|
||||
// */
|
||||
// public String guildId: "",
|
||||
// /**
|
||||
// * 子频道ID集
|
||||
// */
|
||||
// public List<String> channelIds: new ArrayList<>(),
|
||||
|
||||
// 是否将游戏里的聊天转发到群聊
|
||||
"isSendToBot": true,
|
||||
|
||||
// 是否接收群消息并发送到游戏里
|
||||
"isSendToGame": true,
|
||||
|
||||
// // 管理员账号
|
||||
// "adminId": 0,
|
||||
|
||||
// 是否启用登录消息
|
||||
// 当玩家登录服务器时,发送消息通知到群里
|
||||
"sendLoginMessageToBot": true,
|
||||
|
||||
// 玩家登录服务器消息格式
|
||||
// {nickName} 为玩家昵称
|
||||
// {uid} 为玩家UID
|
||||
"loginMessageFormat": "{nickName}({uid}) 加入了服务器",
|
||||
|
||||
// 是否启用登出消息
|
||||
// 当玩家离开服务器时,发送消息通知到群里
|
||||
"sendLogoutMessageToBot": true,
|
||||
|
||||
// 玩家登出服务器消息格式
|
||||
// {nickName} 为玩家昵称
|
||||
// {uid} 为玩家UID
|
||||
"logoutMessageFormat": "{nickName}({uid}) 离开了服务器",
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -3,7 +3,7 @@ plugins {
|
||||
}
|
||||
|
||||
group 'com.github.jie65535.openchat'
|
||||
version 'dev-0.1.0'
|
||||
version 'dev-0.2.0'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
BIN
doc/Chat-OneBot.png
Normal file
BIN
doc/Chat-OneBot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 829 KiB |
286
src/main/java/com/github/jie65535/minionebot/MiniOneBot.java
Normal file
286
src/main/java/com/github/jie65535/minionebot/MiniOneBot.java
Normal file
@ -0,0 +1,286 @@
|
||||
/*
|
||||
* MiniOneBot
|
||||
* Copyright (C) 2023 jie65535
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.github.jie65535.minionebot;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import emu.grasscutter.utils.JsonUtils;
|
||||
import io.javalin.Javalin;
|
||||
import org.slf4j.Logger;
|
||||
import com.github.jie65535.minionebot.events.GroupMessage;
|
||||
import com.github.jie65535.minionebot.events.GroupMessageHandler;
|
||||
import com.github.jie65535.minionebot.events.GuildChannelMessage;
|
||||
import com.github.jie65535.minionebot.events.GuildChannelMessageHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class MiniOneBot implements WsStream.WsMessageHandler {
|
||||
private final Logger logger;
|
||||
private final Javalin javalin;
|
||||
private final String token;
|
||||
private MiniOneBotWsServer server;
|
||||
// private MiniOneBotWsClient client;
|
||||
|
||||
public MiniOneBot(Javalin javalin, String token, Logger logger) {
|
||||
this.javalin = javalin;
|
||||
this.token = token;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
// region WebSocket
|
||||
|
||||
public void startWsServer(String path) {
|
||||
if (server == null) {
|
||||
logger.debug("Start MiniOneBot WebSocket Server");
|
||||
server = new MiniOneBotWsServer(javalin, path, token, logger);
|
||||
server.subscribe(this);
|
||||
}
|
||||
}
|
||||
|
||||
// public void startWsClient(URI serverUri) {
|
||||
// if (client == null) {
|
||||
// logger.info("Start MiniOneBot WebSocket Client");
|
||||
// client = MiniOneBotWsClient.create(serverUri, token, logger);
|
||||
// client.subscribe(this);
|
||||
// }
|
||||
// }
|
||||
|
||||
public void stop() {
|
||||
// if (client != null) {
|
||||
// client.close();
|
||||
// }
|
||||
if (server != null) {
|
||||
try {
|
||||
server.close();
|
||||
} catch (IOException e) {
|
||||
logger.error("Stop MiniOneBot WebSocket Server Failed!", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void sendMessageToAll(String message) {
|
||||
logger.debug("Sending... message=\"{}\"", message);
|
||||
server.send(message);
|
||||
// client.send(message);
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region OneBot message
|
||||
|
||||
@Override
|
||||
public void onMessage(String message) {
|
||||
var map = JsonUtils.decode(message, JsonObject.class);
|
||||
if (!map.has("post_type")) return;
|
||||
var postType = map.get("post_type").getAsString();
|
||||
// 消息事件上报
|
||||
if (Objects.equals(postType, "message")) {
|
||||
var messageType = map.get("message_type").getAsString();
|
||||
var subType = map.get("sub_type").getAsString();
|
||||
|
||||
// 发送者信息 https://docs.go-cqhttp.org/reference/data_struct.html#post-message-messagesender
|
||||
var sender = map.get("sender").getAsJsonObject();
|
||||
var senderId = sender.get("user_id").getAsLong();
|
||||
var senderNickname = sender.get("nickname").getAsString();
|
||||
|
||||
// 群消息上报 https://docs.go-cqhttp.org/event/#%E7%BE%A4%E6%B6%88%E6%81%AF
|
||||
if (Objects.equals(messageType, "group")
|
||||
&& Objects.equals(subType, "normal")) {
|
||||
var groupId = map.get("group_id").getAsLong();
|
||||
// var message = (List<Map<?, ?>>)map.get("message");
|
||||
var rawMessage = map.get("raw_message").getAsString();
|
||||
|
||||
var senderCard = sender.get("card").getAsString();
|
||||
var senderLevel = sender.get("level").getAsString();
|
||||
var senderRole = sender.get("role").getAsString();
|
||||
var senderCardOrNickname = senderCard == null || senderCard.isEmpty() ? senderNickname : senderCard;
|
||||
var senderTitle = sender.get("title").getAsString();
|
||||
onGroupMessage(groupId, handleRawMessage(rawMessage), senderId, senderCardOrNickname, senderLevel, senderRole, senderTitle);
|
||||
}
|
||||
// 频道消息上报 https://docs.go-cqhttp.org/event/guild.html#%E6%94%B6%E5%88%B0%E9%A2%91%E9%81%93%E6%B6%88%E6%81%AF
|
||||
else if (Objects.equals(messageType, "guild")
|
||||
&& Objects.equals(subType, "channel")) {
|
||||
var guildId = map.get("guild_id").getAsString();
|
||||
var channelId = map.get("channel_id").getAsString();
|
||||
var rawMessage = map.get("message").getAsString(); // 需要 Message 消息链类型处理
|
||||
var tinyId = map.get("user_id").getAsString();
|
||||
onGuildMessage(guildId, channelId, handleRawMessage(rawMessage), tinyId, senderNickname);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当收到群消息时触发
|
||||
* @param groupId 群号
|
||||
* @param message 消息
|
||||
* @param senderId 发送者ID
|
||||
* @param senderCardOrNickname 发送者群名片或昵称,名片为空时是昵称
|
||||
* @param senderLevel 发送者群等级
|
||||
* @param senderRole 发送者群身份
|
||||
* @param senderTitle 发送者群专属头衔
|
||||
*/
|
||||
private void onGroupMessage(long groupId,
|
||||
String message,
|
||||
long senderId,
|
||||
String senderCardOrNickname,
|
||||
String senderLevel,
|
||||
String senderRole,
|
||||
String senderTitle) {
|
||||
logger.debug("groupId={}, message={}, senderId={}, senderCardOrNickname={}, senderLevel={}, senderRole={}, senderTitle={}",
|
||||
groupId, message, senderId, senderCardOrNickname, senderLevel, senderRole, senderTitle);
|
||||
groupMessageHandler.handleGroupMessage(new GroupMessage(groupId, message, senderId, senderCardOrNickname, senderLevel, senderRole, senderTitle));
|
||||
}
|
||||
|
||||
/**
|
||||
* 当收到频道消息时触发
|
||||
* @param guildId 频道Id
|
||||
* @param channelId 子频道Id
|
||||
* @param message 消息
|
||||
* @param senderId 发送者Id
|
||||
* @param senderName 发送者昵称
|
||||
*/
|
||||
private void onGuildMessage(String guildId, String channelId, String message, String senderId, String senderName) {
|
||||
logger.debug("guildId={}, channelId={}, message={}, senderId={}, senderNickname={}",
|
||||
guildId, channelId, message, senderId, senderName);
|
||||
guildChannelMessageHandler.handleGuildChannelMessage(new GuildChannelMessage(guildId, channelId, message, senderId, senderName));
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Message API
|
||||
|
||||
// region Models
|
||||
private static class Action {
|
||||
public String action;
|
||||
public Object params;
|
||||
public Action(String action, Object params) {
|
||||
this.action = action;
|
||||
this.params = params;
|
||||
}
|
||||
}
|
||||
|
||||
private static class SendGroupMsgArgs {
|
||||
public long group_id;
|
||||
public String message;
|
||||
public boolean auto_escape = true;
|
||||
public SendGroupMsgArgs(long groupId, String message) {
|
||||
this.group_id = groupId;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
private static class SendGuildChannelMsgArgs {
|
||||
public String guild_id;
|
||||
public String channel_id;
|
||||
public String message;
|
||||
public boolean auto_escape = true;
|
||||
public SendGuildChannelMsgArgs(String guildId, String channelId, String message) {
|
||||
this.guild_id = guildId;
|
||||
this.channel_id = channelId;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
/**
|
||||
* 发送消息到群
|
||||
* @param groupId 群号
|
||||
* @param message 消息
|
||||
*/
|
||||
public void sendGroupMessage(long groupId, String message) {
|
||||
sendMessageToAll(JsonUtils.encode(new Action("send_group_msg", new SendGroupMsgArgs(groupId, message))));
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息到子频道
|
||||
* @param guildId 频道ID
|
||||
* @param channelId 子频道ID
|
||||
* @param message 消息
|
||||
*/
|
||||
public void sendGuildChannelMessage(String guildId, String channelId, String message) {
|
||||
sendMessageToAll(JsonUtils.encode(new Action("send_guild_channel_msg", new SendGuildChannelMsgArgs(guildId, channelId, message))));
|
||||
}
|
||||
|
||||
GroupMessageHandler groupMessageHandler;
|
||||
|
||||
GuildChannelMessageHandler guildChannelMessageHandler;
|
||||
|
||||
/**
|
||||
* 订阅群消息事件
|
||||
* @param handler 群消息处理器
|
||||
*/
|
||||
public void subscribeGroupMessageEvent(GroupMessageHandler handler) {
|
||||
groupMessageHandler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅频道消息事件
|
||||
* @param handler 频道消息处理器
|
||||
*/
|
||||
public void subscribeGuildChannelMessageEvent(GuildChannelMessageHandler handler) {
|
||||
guildChannelMessageHandler = handler;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Utils
|
||||
|
||||
private static final Pattern cqCodePattern = Pattern.compile("\\[CQ:(\\w+).*?]");
|
||||
|
||||
private static String handleRawMessage(String rawMessage) {
|
||||
if (rawMessage.indexOf('[') == -1)
|
||||
return unescape(rawMessage);
|
||||
var message = new StringBuilder();
|
||||
var matcher = cqCodePattern.matcher(rawMessage);
|
||||
while (matcher.find()) {
|
||||
var type = matcher.group(1);
|
||||
var replacement = switch (type) {
|
||||
case "image" -> "[图片]";
|
||||
case "reply" -> "[回复]";
|
||||
case "at" -> "[@]";
|
||||
case "record" -> "[语音]";
|
||||
case "forward" -> "[合并转发]";
|
||||
case "video" -> "[视频]";
|
||||
case "music" -> "[音乐]";
|
||||
case "redbag" -> "[红包]";
|
||||
case "poke" -> "[戳一戳]";
|
||||
default -> "";
|
||||
};
|
||||
matcher.appendReplacement(message, replacement);
|
||||
}
|
||||
matcher.appendTail(message);
|
||||
return unescape(message.toString());
|
||||
}
|
||||
|
||||
// private static String escape(String msg) {
|
||||
// return msg.replace("&", "&")
|
||||
// .replace("[", "[")
|
||||
// .replace("]", "]")
|
||||
// .replace(",", ",");
|
||||
// }
|
||||
|
||||
private static String unescape(String msg) {
|
||||
return msg.replace("&", "&")
|
||||
.replace("[", "[")
|
||||
.replace("]", "]")
|
||||
.replace(",", ",");
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
/*
|
||||
* MiniOneBot
|
||||
* Copyright (C) 2023 jie65535
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
//package com.github.jie65535.minionebot;
|
||||
//
|
||||
//import org.java_websocket.client.WebSocketClient;
|
||||
//import org.java_websocket.handshake.ServerHandshake;
|
||||
//import org.slf4j.Logger;
|
||||
//
|
||||
//import java.net.URI;
|
||||
//import java.util.HashMap;
|
||||
//import java.util.Map;
|
||||
//import java.util.Timer;
|
||||
//import java.util.TimerTask;
|
||||
//
|
||||
//public class MiniOneBotWsClient extends WebSocketClient implements WsStream {
|
||||
// private final Logger logger;
|
||||
//
|
||||
// private MiniOneBotWsClient(URI serverUri, Map<String, String> headers, Logger logger) {
|
||||
// super(serverUri, headers);
|
||||
//
|
||||
// this.logger = logger;
|
||||
// }
|
||||
//
|
||||
// public static MiniOneBotWsClient create(URI serverUri, String token, Logger logger) {
|
||||
// var headers = new HashMap<String, String>();
|
||||
// headers.put("Authorization", "Bearer " + token);
|
||||
// var client = new MiniOneBotWsClient(serverUri, headers, logger);
|
||||
// var wsClientDaemon = new Timer("WsClientDaemon", true);
|
||||
// wsClientDaemon.schedule(new TimerTask() {
|
||||
// @Override
|
||||
// public void run() {
|
||||
// if (!client.isOpen()) {
|
||||
// logger.debug("Try connect...");
|
||||
// client.connect();
|
||||
// }
|
||||
// }
|
||||
// }, 5_000);
|
||||
// return client;
|
||||
// }
|
||||
//
|
||||
// private WsMessageHandler callback;
|
||||
//
|
||||
// @Override
|
||||
// public void subscribe(WsMessageHandler callback) {
|
||||
// this.callback = callback;
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void onOpen(ServerHandshake handshakedata) {
|
||||
// logger.info("onOpen: statusMessage={}", handshakedata.getHttpStatusMessage());
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void onMessage(String message) {
|
||||
// logger.info("onMessage: {}", message);
|
||||
// callback.onMessage(message);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void onClose(int code, String reason, boolean remote) {
|
||||
// logger.info("onClose: code={} reason={} isRemote={}", code, reason, remote);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void onError(Exception ex) {
|
||||
// logger.error("onError:", ex);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void send(String message) {
|
||||
// if (isOpen()) {
|
||||
// super.send(message);
|
||||
// }
|
||||
// }
|
||||
//}
|
@ -0,0 +1,123 @@
|
||||
/*
|
||||
* MiniOneBot
|
||||
* Copyright (C) 2023 jie65535
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.github.jie65535.minionebot;
|
||||
|
||||
import io.javalin.Javalin;
|
||||
import io.javalin.websocket.*;
|
||||
import org.eclipse.jetty.websocket.api.CloseStatus;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class MiniOneBotWsServer implements WsStream, Closeable {
|
||||
|
||||
private final String token;
|
||||
private final Logger logger;
|
||||
private final Map<WsContext, String> connections = new ConcurrentHashMap<>();
|
||||
|
||||
public MiniOneBotWsServer(Javalin javalin, String path, String token, Logger logger) {
|
||||
this.token = token;
|
||||
this.logger = logger;
|
||||
javalin.ws(path, ws -> {
|
||||
ws.onConnect(this::onConnect);
|
||||
ws.onClose(this::onClose);
|
||||
ws.onError(this::onError);
|
||||
ws.onMessage(this::onMessage);
|
||||
});
|
||||
|
||||
logger.info("WebSocket server started at {}", path);
|
||||
}
|
||||
|
||||
public void onConnect(WsConnectContext ctx) {
|
||||
logger.info("onConnect: address={} headers={}", ctx.session.getRemoteAddress(), ctx.headerMap());
|
||||
var author = ctx.header("Authorization");
|
||||
// Check access token.
|
||||
if (author == null) {
|
||||
logger.warn("The connection was closed because the request did not contain an authorization token");
|
||||
ctx.session.close(new CloseStatus(401, "Unauthorized"));
|
||||
} else if (!author.equals("Bearer " + token) && !author.equals("Token " + token)) {
|
||||
logger.warn("Connection closed due to incorrect authorization token in the request");
|
||||
ctx.session.close(new CloseStatus(403, "Unauthorized"));
|
||||
} else {
|
||||
var selfId = ctx.header("X-Self-ID");
|
||||
if (selfId != null && !selfId.isEmpty()) {
|
||||
logger.info("Bot [{}] WebSocket connected", selfId);
|
||||
} else {
|
||||
logger.info("[{}] WebSocket connected", ctx.session.getRemoteAddress());
|
||||
}
|
||||
connections.put(ctx, selfId);
|
||||
}
|
||||
}
|
||||
|
||||
public void onClose(WsCloseContext ctx) {
|
||||
logger.debug("onClose: address={} status={} reason={}", ctx.session.getRemoteAddress(), ctx.status(), ctx.reason());
|
||||
var selfId = connections.remove(ctx);
|
||||
if (selfId != null && !selfId.isEmpty()) {
|
||||
logger.warn("Bot [{}] WebSocket disconnected, status={} reason={}", selfId, ctx.status(), ctx.reason());
|
||||
} else {
|
||||
logger.warn("[{}] WebSocket disconnected, status={} reason={}", ctx.session.getRemoteAddress(), ctx.status(), ctx.reason());
|
||||
}
|
||||
}
|
||||
|
||||
public void onError(WsErrorContext ctx) {
|
||||
logger.debug("onError: address={}", ctx.session.getRemoteAddress(), ctx.error());
|
||||
var selfId = connections.remove(ctx);
|
||||
if (selfId != null && !selfId.isEmpty()) {
|
||||
logger.warn("Bot [{}] WebSocket disconnected", selfId, ctx.error());
|
||||
} else {
|
||||
logger.warn("[{}] WebSocket disconnected", ctx.session.getRemoteAddress(), ctx.error());
|
||||
}
|
||||
}
|
||||
|
||||
public void onMessage(WsMessageContext ctx) {
|
||||
logger.debug("onMessage: {}", ctx.message());
|
||||
|
||||
callback.onMessage(ctx.message());
|
||||
}
|
||||
|
||||
private WsMessageHandler callback;
|
||||
|
||||
@Override
|
||||
public void subscribe(WsMessageHandler callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(String message) {
|
||||
if (connections.isEmpty()) return;
|
||||
for (var ctx : connections.keySet()) {
|
||||
if (ctx.session.isOpen()) {
|
||||
ctx.send(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (connections.isEmpty()) return;
|
||||
for (var ctx : connections.keySet()) {
|
||||
if (ctx.session.isOpen()) {
|
||||
ctx.session.close(1001, "Service stopped");
|
||||
}
|
||||
}
|
||||
connections.clear();
|
||||
}
|
||||
}
|
28
src/main/java/com/github/jie65535/minionebot/WsStream.java
Normal file
28
src/main/java/com/github/jie65535/minionebot/WsStream.java
Normal file
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* MiniOneBot
|
||||
* Copyright (C) 2023 jie65535
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.github.jie65535.minionebot;
|
||||
|
||||
public interface WsStream {
|
||||
void subscribe(WsMessageHandler callback);
|
||||
|
||||
void send(String message);
|
||||
|
||||
interface WsMessageHandler {
|
||||
void onMessage(String message);
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* MiniOneBot
|
||||
* Copyright (C) 2023 jie65535
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.github.jie65535.minionebot.events;
|
||||
|
||||
public record GroupMessage(
|
||||
long groupId,
|
||||
String message,
|
||||
long senderId,
|
||||
String senderCardOrNickname,
|
||||
String senderLevel,
|
||||
String senderRole,
|
||||
String senderTitle
|
||||
) {
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
/*
|
||||
* MiniOneBot
|
||||
* Copyright (C) 2023 jie65535
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.github.jie65535.minionebot.events;
|
||||
|
||||
public interface GroupMessageHandler {
|
||||
void handleGroupMessage(GroupMessage event);
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* MiniOneBot
|
||||
* Copyright (C) 2023 jie65535
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.github.jie65535.minionebot.events;
|
||||
|
||||
public record GuildChannelMessage(
|
||||
String guildId,
|
||||
String channelId,
|
||||
String message,
|
||||
String senderId,
|
||||
String senderName
|
||||
) {
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
/*
|
||||
* MiniOneBot
|
||||
* Copyright (C) 2023 jie65535
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.github.jie65535.minionebot.events;
|
||||
|
||||
public interface GuildChannelMessageHandler {
|
||||
void handleGuildChannelMessage(GuildChannelMessage event);
|
||||
}
|
@ -17,28 +17,13 @@
|
||||
*/
|
||||
package com.github.jie65535.openchat;
|
||||
|
||||
//import emu.grasscutter.server.event.player.PlayerJoinEvent;
|
||||
//import emu.grasscutter.server.scheduler.ServerTaskScheduler;
|
||||
import emu.grasscutter.server.event.player.PlayerJoinEvent;
|
||||
|
||||
//public final class EventListeners {
|
||||
// private static final OpenChatPlugin plugin = OpenChatPlugin.getInstance();
|
||||
// private static final OpenChatConfig config = OpenChatPlugin.getInstance().getConfig();
|
||||
|
||||
// public static void onJoin(PlayerJoinEvent event) {
|
||||
// // 检查聊天系统是否被其它插件替换
|
||||
// // 不再检查
|
||||
//// if (!(plugin.getServer().getChatSystem() instanceof OpenChatSystem)) {
|
||||
//// plugin.getLogger().warn("聊天系统已被其它插件更改,现已重置为 OpenChat !");
|
||||
//// plugin.getServer().setChatSystem(new OpenChatSystem(plugin));
|
||||
//// }
|
||||
//
|
||||
// if (!config.sendJoinMessage || config.joinMessage.isEmpty())
|
||||
// return;
|
||||
// var player = event.getPlayer();
|
||||
// plugin.getLogger().debug(String.format("Player %s(%d) joined the game, send join message.",
|
||||
// player.getNickname(), player.getUid()));
|
||||
// plugin.getServer().getScheduler().scheduleDelayedTask(() -> {
|
||||
// if (player.isOnline()) player.dropMessage(config.joinMessage);
|
||||
// }, 60);
|
||||
// }
|
||||
//}
|
||||
public final class EventListeners {
|
||||
public static void onJoin(PlayerJoinEvent event) {
|
||||
var cs = OpenChatPlugin.getInstance().getServer().getChatSystem();
|
||||
if (cs instanceof OpenChatSystem) {
|
||||
((OpenChatSystem) cs).onPlayerJoin(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -57,4 +57,104 @@ public class OpenChatConfig {
|
||||
* {limit} 服务器设置的限制次数
|
||||
*/
|
||||
public String msgTooFrequentFeedback = "服务器设置每分钟仅允许发言{limit}次";
|
||||
|
||||
/**
|
||||
* 将聊天log
|
||||
*/
|
||||
public boolean logChat = true;
|
||||
|
||||
/**
|
||||
* WebSocket Access Token
|
||||
* 安全令牌,仅允许授权的连接
|
||||
* 如果为空将会在启动时自动生成一个32位随机令牌并显示在控制台
|
||||
*/
|
||||
public String wsToken = "";
|
||||
|
||||
/**
|
||||
* WebSocket Path
|
||||
* 反向WS的路径,即机器人连接到本插件开放的WS接口路径
|
||||
* 若不想开放WS,则留空,默认为 /openchat
|
||||
* OneBot设置示例:ws://127.0.0.1:443/openchat
|
||||
*/
|
||||
public String wsPath = "/openchat";
|
||||
|
||||
// /**
|
||||
// * WebSocket Address
|
||||
// * 正向WS的地址,即本插件主动连接机器人开放的WS接口地址
|
||||
// * 示例:ws://127.0.0.1:8080
|
||||
// * 若不需要,则留空
|
||||
// * TODO:由于需要引入外部依赖,正向WS方式暂不实现
|
||||
// */
|
||||
// public String wsAddress = "";
|
||||
|
||||
/**
|
||||
* 群ID
|
||||
*/
|
||||
public Long groupId = 0L;
|
||||
|
||||
/**
|
||||
* 群消息格式化
|
||||
* {id} 为QQ号
|
||||
* {name} 为群名片,如果为空则显示昵称
|
||||
* {message} 为消息
|
||||
*/
|
||||
public String groupToGameFormat = "<color=#6699CC>[QQ]</color><color=#99CC99>{name}</color>: {message}";
|
||||
|
||||
/**
|
||||
* 服务器聊天消息格式
|
||||
* {nickName} 为玩家昵称
|
||||
* {uid} 为玩家UID
|
||||
* {message} 为消息内容
|
||||
*/
|
||||
public String gameToGroupFormat = "[GC]{nickName}({uid}): {message}";
|
||||
|
||||
// /**
|
||||
// * 频道ID
|
||||
// */
|
||||
// public String guildId = "";
|
||||
// /**
|
||||
// * 子频道ID集
|
||||
// */
|
||||
// public List<String> channelIds = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 是否将游戏里的聊天转发到群聊
|
||||
*/
|
||||
public boolean isSendToBot = true;
|
||||
|
||||
/**
|
||||
* 是否接收群消息并发送到游戏里
|
||||
*/
|
||||
public boolean isSendToGame = true;
|
||||
|
||||
// /**
|
||||
// * 管理员账号
|
||||
// */
|
||||
// public Long adminId = 0L;
|
||||
|
||||
/**
|
||||
* 是否启用登录消息
|
||||
* 当玩家登录服务器时,发送消息通知到群里
|
||||
*/
|
||||
public boolean sendLoginMessageToBot = true;
|
||||
|
||||
/**
|
||||
* 玩家登录服务器消息格式
|
||||
* {nickName} 为玩家昵称
|
||||
* {uid} 为玩家UID
|
||||
*/
|
||||
public String loginMessageFormat = "{nickName}({uid}) 加入了服务器";
|
||||
|
||||
/**
|
||||
* 是否启用登出消息
|
||||
* 当玩家离开服务器时,发送消息通知到群里
|
||||
*/
|
||||
public boolean sendLogoutMessageToBot = true;
|
||||
|
||||
/**
|
||||
* 玩家登出服务器消息格式
|
||||
* {nickName} 为玩家昵称
|
||||
* {uid} 为玩家UID
|
||||
*/
|
||||
public String logoutMessageFormat = "{nickName}({uid}) 离开了服务器";
|
||||
}
|
||||
|
@ -20,6 +20,9 @@ package com.github.jie65535.openchat;
|
||||
import com.github.jie65535.openchat.commands.ChatPlayerCommands;
|
||||
import com.github.jie65535.openchat.commands.ChatServerCommands;
|
||||
import emu.grasscutter.plugin.Plugin;
|
||||
import emu.grasscutter.server.event.EventHandler;
|
||||
import emu.grasscutter.server.event.HandlerPriority;
|
||||
import emu.grasscutter.server.event.player.PlayerJoinEvent;
|
||||
import emu.grasscutter.utils.JsonUtils;
|
||||
|
||||
import java.io.File;
|
||||
@ -101,10 +104,10 @@ public final class OpenChatPlugin extends Plugin {
|
||||
@Override
|
||||
public void onEnable() {
|
||||
// Register event listeners.
|
||||
// new EventHandler<>(PlayerJoinEvent.class)
|
||||
// .priority(HandlerPriority.NORMAL)
|
||||
// .listener(EventListeners::onJoin)
|
||||
// .register(this);
|
||||
new EventHandler<>(PlayerJoinEvent.class)
|
||||
.priority(HandlerPriority.NORMAL)
|
||||
.listener(EventListeners::onJoin)
|
||||
.register(this);
|
||||
|
||||
// Register commands.
|
||||
getHandle().registerCommand(new ChatServerCommands());
|
||||
|
@ -17,64 +17,134 @@
|
||||
*/
|
||||
package com.github.jie65535.openchat;
|
||||
|
||||
import com.github.jie65535.minionebot.MiniOneBot;
|
||||
import com.github.jie65535.minionebot.events.GroupMessage;
|
||||
import emu.grasscutter.GameConstants;
|
||||
import emu.grasscutter.game.chat.ChatSystem;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.server.event.player.PlayerJoinEvent;
|
||||
import emu.grasscutter.utils.Crypto;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||
import it.unimi.dsi.fastutil.ints.IntSet;
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
public class OpenChatSystem extends ChatSystem {
|
||||
private final OpenChatPlugin plugin;
|
||||
private final Logger logger;
|
||||
private final MiniOneBot miniOneBot;
|
||||
|
||||
public OpenChatSystem(OpenChatPlugin plugin) {
|
||||
super(plugin.getServer());
|
||||
this.plugin = plugin;
|
||||
plugin.getLogger().debug("OpenChatSystem created.");
|
||||
this.logger = plugin.getLogger();
|
||||
|
||||
// 获取HttpServer框架
|
||||
var javalin = plugin.getHandle().getHttpServer().getHandle();
|
||||
var token = plugin.getConfig().wsToken;
|
||||
if (token == null || token.isEmpty()) {
|
||||
token = Utils.base64Encode(Crypto.createSessionKey(24));
|
||||
plugin.getConfig().wsToken = token;
|
||||
plugin.saveConfig();
|
||||
logger.warn("Detected that wsToken is empty, automatically generated Token for you as follows: {}", token);
|
||||
}
|
||||
// 构造MiniOneBot
|
||||
miniOneBot = new MiniOneBot(javalin, token, logger);
|
||||
// 启动WebSocket服务
|
||||
miniOneBot.startWsServer(plugin.getConfig().wsPath);
|
||||
// 订阅群消息事件
|
||||
miniOneBot.subscribeGroupMessageEvent(this::onGroupMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家进入服务器时触发
|
||||
*
|
||||
* @param event 事件
|
||||
*/
|
||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||
if (!plugin.getConfig().sendLoginMessageToBot || plugin.getConfig().groupId < 1) return;
|
||||
var player = event.getPlayer();
|
||||
miniOneBot.sendGroupMessage(plugin.getConfig().groupId, plugin.getConfig().loginMessageFormat
|
||||
.replace("{nickName}", player.getNickname())
|
||||
.replace("{uid}", String.valueOf(player.getUid())));
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于标识玩家是否首次获取聊天记录
|
||||
*/
|
||||
IntSet hasHistory = new IntOpenHashSet();
|
||||
|
||||
/**
|
||||
* 在登出时清理聊天记录
|
||||
*
|
||||
* @param player 登出玩家
|
||||
*/
|
||||
@Override
|
||||
public void clearHistoryOnLogout(Player player) {
|
||||
super.clearHistoryOnLogout(player);
|
||||
hasHistory.remove(player.getUid());
|
||||
|
||||
// 发送离线消息
|
||||
if (!plugin.getConfig().sendLogoutMessageToBot || plugin.getConfig().groupId < 1) return;
|
||||
miniOneBot.sendGroupMessage(plugin.getConfig().groupId, plugin.getConfig().logoutMessageFormat
|
||||
.replace("{nickName}", player.getNickname())
|
||||
.replace("{uid}", String.valueOf(player.getUid())));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理拉取聊天记录请求
|
||||
*
|
||||
* @param player 拉取聊天记录的玩家
|
||||
*/
|
||||
@Override
|
||||
public void handlePullRecentChatReq(Player player) {
|
||||
super.handlePullRecentChatReq(player);
|
||||
if (!hasHistory.contains(player.getUid())) {
|
||||
// 如果是首次拉取,则向玩家发送欢迎消息
|
||||
hasHistory.add(player.getUid());
|
||||
if (plugin.getConfig().sendJoinMessage && !plugin.getConfig().joinMessage.isEmpty()) {
|
||||
plugin.getLogger().debug(String.format("send join message to %s(%d)",
|
||||
player.getNickname(), player.getUid()));
|
||||
logger.debug("send join message to {}({})",
|
||||
player.getNickname(), player.getUid());
|
||||
player.dropMessage(plugin.getConfig().joinMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重载父类发送私聊消息方法
|
||||
* 将普通聊天消息接出由本系统处理
|
||||
*
|
||||
* @param player 发言玩家
|
||||
* @param targetUid 目标玩家Uid
|
||||
* @param message 消息内容
|
||||
*/
|
||||
@Override
|
||||
public void sendPrivateMessage(Player player, int targetUid, String message) {
|
||||
plugin.getLogger().debug(String.format("onSendPrivateMessage: player=%s(%d) targetUid=%d message=%s",
|
||||
player.getNickname(), player.getUid(), targetUid, message));
|
||||
logger.debug("onSendPrivateMessage: player={}({}) targetUid={} message={}",
|
||||
player.getNickname(), player.getUid(), targetUid, message);
|
||||
// Sanity checks.
|
||||
if (message == null || message.length() == 0) {
|
||||
return;
|
||||
}
|
||||
// 调用父类发送消息方法
|
||||
super.sendPrivateMessage(player, targetUid, message);
|
||||
|
||||
// 如果目标不是服务器,或者消息是命令,则忽略
|
||||
if (targetUid != GameConstants.SERVER_CONSOLE_UID || message.charAt(0) == '/' || message.charAt(0) == '!') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 否则执行玩家任意消息方法
|
||||
handlePlayerMessage(player, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理玩家消息
|
||||
* @param player 玩家对象
|
||||
*
|
||||
* @param player 玩家对象
|
||||
* @param message 消息内容
|
||||
*/
|
||||
private void handlePlayerMessage(Player player, String message) {
|
||||
@ -85,8 +155,8 @@ public class OpenChatSystem extends ChatSystem {
|
||||
|
||||
// 检测是否正在禁言中
|
||||
if (checkIsBanning(player)) {
|
||||
plugin.getLogger().warn(String.format("Message blocked (banning): player=%s(%d): \"%s\"",
|
||||
player.getNickname(), player.getUid(), message));
|
||||
logger.warn("Message blocked (banning): player={}({}): \"{}\"",
|
||||
player.getNickname(), player.getUid(), message);
|
||||
if (!plugin.getConfig().bannedFeedback.isEmpty()) {
|
||||
player.dropMessage(plugin.getConfig().bannedFeedback);
|
||||
}
|
||||
@ -96,8 +166,8 @@ public class OpenChatSystem extends ChatSystem {
|
||||
// 处理发言频率限制
|
||||
if (!checkMessageFre(player)) {
|
||||
// 可提示也可忽略,忽略可让玩家以为自己发送成功,其实别人看不到
|
||||
plugin.getLogger().warn(String.format("Message blocked (too often): player=%s(%d): \"%s\"",
|
||||
player.getNickname(), player.getUid(), message));
|
||||
logger.warn("Message blocked (too often): player={}({}): \"{}\"",
|
||||
player.getNickname(), player.getUid(), message);
|
||||
if (!plugin.getConfig().msgTooFrequentFeedback.isEmpty()) {
|
||||
player.dropMessage(
|
||||
plugin.getConfig().msgTooFrequentFeedback
|
||||
@ -108,25 +178,80 @@ public class OpenChatSystem extends ChatSystem {
|
||||
|
||||
// 处理发言内容审查
|
||||
if (!checkMessageModeration(message)) {
|
||||
plugin.getLogger().warn(String.format("Message blocked (moderation): player=%s(%d): \"%s\"",
|
||||
player.getNickname(), player.getUid(), message));
|
||||
logger.warn("Message blocked (moderation): player={}({}): \"{}\"",
|
||||
player.getNickname(), player.getUid(), message);
|
||||
return;
|
||||
}
|
||||
|
||||
// log messages
|
||||
if (plugin.getConfig().logChat) {
|
||||
logger.info("{}({}): \"{}\"",
|
||||
player.getNickname(), player.getUid(), message);
|
||||
}
|
||||
|
||||
// 格式化消息
|
||||
message = OpenChatPlugin.getInstance().getConfig().serverChatFormat
|
||||
var formattedMessage = OpenChatPlugin.getInstance().getConfig().serverChatFormat
|
||||
.replace("{nickName}", player.getNickname())
|
||||
.replace("{uid}", String.valueOf(player.getUid()))
|
||||
.replace("{message}", message);
|
||||
|
||||
// 转发给其它玩家
|
||||
for (Player p : getServer().getPlayers().values()) {
|
||||
// 将消息发送给除了自己以外所有未关闭聊天的玩家
|
||||
if (p != player && !plugin.getData().offChatPlayers.contains(p.getUid())) {
|
||||
p.dropMessage(formattedMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// 转发到机器人
|
||||
if (!plugin.getConfig().isSendToBot || plugin.getConfig().groupId < 1) return;
|
||||
miniOneBot.sendGroupMessage(plugin.getConfig().groupId, plugin.getConfig().gameToGroupFormat
|
||||
.replace("{nickName}", player.getNickname())
|
||||
.replace("{uid}", String.valueOf(player.getUid()))
|
||||
.replace("{message}", message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 收到群消息时触发
|
||||
*
|
||||
* @param event 群消息事件
|
||||
*/
|
||||
private void onGroupMessage(GroupMessage event) {
|
||||
if (!plugin.getConfig().isSendToGame
|
||||
|| plugin.getConfig().groupId < 1
|
||||
|| event.groupId() != plugin.getConfig().groupId
|
||||
) return;
|
||||
|
||||
// log messages
|
||||
if (plugin.getConfig().logChat) {
|
||||
logger.info("[MiniOneBot] {}: \"{}\"",
|
||||
event.senderCardOrNickname(), event.message());
|
||||
}
|
||||
broadcastChatMessage(plugin.getConfig().groupToGameFormat
|
||||
.replace("{id}", String.valueOf(event.senderId()))
|
||||
.replace("{name}", event.senderCardOrNickname())
|
||||
.replace("{message}", event.message()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播聊天消息给所有玩家(未开启聊天玩家除外)
|
||||
*
|
||||
* @param message 纯文本消息
|
||||
*/
|
||||
public void broadcastChatMessage(String message) {
|
||||
for (Player p : getServer().getPlayers().values()) {
|
||||
if (!plugin.getData().offChatPlayers.contains(p.getUid())) {
|
||||
p.dropMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查玩家是否正在禁言中
|
||||
*
|
||||
* @param player 玩家对象
|
||||
* @return 是否禁言中
|
||||
*/
|
||||
private boolean checkIsBanning(Player player) {
|
||||
var banList = plugin.getData().banList;
|
||||
// 检测是否正在禁言中
|
||||
@ -142,6 +267,7 @@ public class OpenChatSystem extends ChatSystem {
|
||||
|
||||
/**
|
||||
* 消息内容审查
|
||||
*
|
||||
* @param message 消息
|
||||
* @return 是否合法合规
|
||||
*/
|
||||
@ -150,10 +276,16 @@ public class OpenChatSystem extends ChatSystem {
|
||||
return !message.isEmpty();
|
||||
}
|
||||
|
||||
// region 发言频率限制
|
||||
|
||||
/**
|
||||
* 发言频率计时器
|
||||
*/
|
||||
Int2ObjectMap<LongArrayFIFOQueue> speakingTimes = new Int2ObjectOpenHashMap<>();
|
||||
|
||||
/**
|
||||
* 消息频率检查
|
||||
*
|
||||
* @param player 玩家对象
|
||||
* @return 是否在约定阈值内
|
||||
*/
|
||||
@ -166,4 +298,6 @@ public class OpenChatSystem extends ChatSystem {
|
||||
list.dequeueLong();
|
||||
return list.size() <= plugin.getConfig().messageFreLimitPerMinute;
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
@ -26,7 +26,14 @@ import java.util.List;
|
||||
|
||||
@Command(label = "serverchat",
|
||||
aliases = { "sc" },
|
||||
usage = { "on/off", "unban|unmute @<UID>", "ban|mute @<UID> [time(Minutes)]", "limit <timesPerMinute>", "reload" },
|
||||
usage = {
|
||||
"on/off",
|
||||
"unban|unmute @<UID>",
|
||||
"ban|mute @<UID> [time(Minutes)]",
|
||||
"limit <timesPerMinute>",
|
||||
"reload",
|
||||
"group <groupId>",
|
||||
},
|
||||
permission = "server.chat",
|
||||
permissionTargeted = "server.chat.others",
|
||||
targetRequirement = Command.TargetRequirement.NONE)
|
||||
@ -98,6 +105,18 @@ public class ChatServerCommands implements CommandHandler {
|
||||
plugin.loadConfig();
|
||||
CommandHandler.sendMessage(sender, "OK");
|
||||
}
|
||||
case "group" -> {
|
||||
var groupId = 0L;
|
||||
try {
|
||||
groupId = Long.parseLong(args.get(1));
|
||||
} catch (NumberFormatException ignored) {
|
||||
sendUsageMessage(sender);
|
||||
return;
|
||||
}
|
||||
plugin.getConfig().groupId = groupId;
|
||||
plugin.saveConfig();
|
||||
CommandHandler.sendMessage(sender, "OK");
|
||||
}
|
||||
default -> sendUsageMessage(sender);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user