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"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<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">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
|
111
README.md
111
README.md
@ -18,9 +18,10 @@
|
|||||||
- [x] 玩家间聊天
|
- [x] 玩家间聊天
|
||||||
- [x] 聊天管理命令
|
- [x] 聊天管理命令
|
||||||
- [x] 发言频率限制
|
- [x] 发言频率限制
|
||||||
|
- [x] 聊天api (OneBot)
|
||||||
|
- [x] 玩家上下线提醒
|
||||||
|
- [ ] 指定管理账号在群内执行控制台命令
|
||||||
- [ ] 聊天内容审查
|
- [ ] 聊天内容审查
|
||||||
- [ ] 聊天api _(~~OneBot api~~)_
|
|
||||||
- [ ] ...
|
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
@ -39,37 +40,125 @@
|
|||||||
- `/serverchat unban @uid` 解除指定玩家禁言
|
- `/serverchat unban @uid` 解除指定玩家禁言
|
||||||
- `/serverchat limit <次每分钟>` 设置发消息频率限制
|
- `/serverchat limit <次每分钟>` 设置发消息频率限制
|
||||||
- `/serverchat reload` 重载配置文件
|
- `/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
|
```json5
|
||||||
{
|
{
|
||||||
// 服务器聊天开关
|
// 服务器聊天开关
|
||||||
serverChatEnabled: true,
|
"serverChatEnabled": true,
|
||||||
|
|
||||||
// 服务器聊天消息格式
|
// 服务器聊天消息格式
|
||||||
// {nickName} 为玩家昵称
|
// {nickName} 为玩家昵称
|
||||||
// {uid} 为玩家UID
|
// {uid} 为玩家UID
|
||||||
// {message} 为消息内容
|
// {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} 服务器设置的限制次数
|
// {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'
|
group 'com.github.jie65535.openchat'
|
||||||
version 'dev-0.1.0'
|
version 'dev-0.2.0'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
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;
|
package com.github.jie65535.openchat;
|
||||||
|
|
||||||
//import emu.grasscutter.server.event.player.PlayerJoinEvent;
|
import emu.grasscutter.server.event.player.PlayerJoinEvent;
|
||||||
//import emu.grasscutter.server.scheduler.ServerTaskScheduler;
|
|
||||||
|
|
||||||
//public final class EventListeners {
|
public final class EventListeners {
|
||||||
// private static final OpenChatPlugin plugin = OpenChatPlugin.getInstance();
|
public static void onJoin(PlayerJoinEvent event) {
|
||||||
// private static final OpenChatConfig config = OpenChatPlugin.getInstance().getConfig();
|
var cs = OpenChatPlugin.getInstance().getServer().getChatSystem();
|
||||||
|
if (cs instanceof OpenChatSystem) {
|
||||||
// public static void onJoin(PlayerJoinEvent event) {
|
((OpenChatSystem) cs).onPlayerJoin(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);
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
@ -57,4 +57,104 @@ public class OpenChatConfig {
|
|||||||
* {limit} 服务器设置的限制次数
|
* {limit} 服务器设置的限制次数
|
||||||
*/
|
*/
|
||||||
public String msgTooFrequentFeedback = "服务器设置每分钟仅允许发言{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.ChatPlayerCommands;
|
||||||
import com.github.jie65535.openchat.commands.ChatServerCommands;
|
import com.github.jie65535.openchat.commands.ChatServerCommands;
|
||||||
import emu.grasscutter.plugin.Plugin;
|
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 emu.grasscutter.utils.JsonUtils;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -101,10 +104,10 @@ public final class OpenChatPlugin extends Plugin {
|
|||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
// Register event listeners.
|
// Register event listeners.
|
||||||
// new EventHandler<>(PlayerJoinEvent.class)
|
new EventHandler<>(PlayerJoinEvent.class)
|
||||||
// .priority(HandlerPriority.NORMAL)
|
.priority(HandlerPriority.NORMAL)
|
||||||
// .listener(EventListeners::onJoin)
|
.listener(EventListeners::onJoin)
|
||||||
// .register(this);
|
.register(this);
|
||||||
|
|
||||||
// Register commands.
|
// Register commands.
|
||||||
getHandle().registerCommand(new ChatServerCommands());
|
getHandle().registerCommand(new ChatServerCommands());
|
||||||
|
@ -17,64 +17,134 @@
|
|||||||
*/
|
*/
|
||||||
package com.github.jie65535.openchat;
|
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.GameConstants;
|
||||||
import emu.grasscutter.game.chat.ChatSystem;
|
import emu.grasscutter.game.chat.ChatSystem;
|
||||||
import emu.grasscutter.game.player.Player;
|
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.Int2ObjectMap;
|
||||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||||
import it.unimi.dsi.fastutil.ints.IntSet;
|
import it.unimi.dsi.fastutil.ints.IntSet;
|
||||||
import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
|
import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
public class OpenChatSystem extends ChatSystem {
|
public class OpenChatSystem extends ChatSystem {
|
||||||
private final OpenChatPlugin plugin;
|
private final OpenChatPlugin plugin;
|
||||||
|
private final Logger logger;
|
||||||
|
private final MiniOneBot miniOneBot;
|
||||||
|
|
||||||
public OpenChatSystem(OpenChatPlugin plugin) {
|
public OpenChatSystem(OpenChatPlugin plugin) {
|
||||||
super(plugin.getServer());
|
super(plugin.getServer());
|
||||||
this.plugin = plugin;
|
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();
|
IntSet hasHistory = new IntOpenHashSet();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在登出时清理聊天记录
|
||||||
|
*
|
||||||
|
* @param player 登出玩家
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void clearHistoryOnLogout(Player player) {
|
public void clearHistoryOnLogout(Player player) {
|
||||||
super.clearHistoryOnLogout(player);
|
super.clearHistoryOnLogout(player);
|
||||||
hasHistory.remove(player.getUid());
|
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
|
@Override
|
||||||
public void handlePullRecentChatReq(Player player) {
|
public void handlePullRecentChatReq(Player player) {
|
||||||
super.handlePullRecentChatReq(player);
|
super.handlePullRecentChatReq(player);
|
||||||
if (!hasHistory.contains(player.getUid())) {
|
if (!hasHistory.contains(player.getUid())) {
|
||||||
|
// 如果是首次拉取,则向玩家发送欢迎消息
|
||||||
hasHistory.add(player.getUid());
|
hasHistory.add(player.getUid());
|
||||||
if (plugin.getConfig().sendJoinMessage && !plugin.getConfig().joinMessage.isEmpty()) {
|
if (plugin.getConfig().sendJoinMessage && !plugin.getConfig().joinMessage.isEmpty()) {
|
||||||
plugin.getLogger().debug(String.format("send join message to %s(%d)",
|
logger.debug("send join message to {}({})",
|
||||||
player.getNickname(), player.getUid()));
|
player.getNickname(), player.getUid());
|
||||||
player.dropMessage(plugin.getConfig().joinMessage);
|
player.dropMessage(plugin.getConfig().joinMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重载父类发送私聊消息方法
|
||||||
|
* 将普通聊天消息接出由本系统处理
|
||||||
|
*
|
||||||
|
* @param player 发言玩家
|
||||||
|
* @param targetUid 目标玩家Uid
|
||||||
|
* @param message 消息内容
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void sendPrivateMessage(Player player, int targetUid, String message) {
|
public void sendPrivateMessage(Player player, int targetUid, String message) {
|
||||||
plugin.getLogger().debug(String.format("onSendPrivateMessage: player=%s(%d) targetUid=%d message=%s",
|
logger.debug("onSendPrivateMessage: player={}({}) targetUid={} message={}",
|
||||||
player.getNickname(), player.getUid(), targetUid, message));
|
player.getNickname(), player.getUid(), targetUid, message);
|
||||||
// Sanity checks.
|
// Sanity checks.
|
||||||
if (message == null || message.length() == 0) {
|
if (message == null || message.length() == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 调用父类发送消息方法
|
||||||
super.sendPrivateMessage(player, targetUid, message);
|
super.sendPrivateMessage(player, targetUid, message);
|
||||||
|
|
||||||
|
// 如果目标不是服务器,或者消息是命令,则忽略
|
||||||
if (targetUid != GameConstants.SERVER_CONSOLE_UID || message.charAt(0) == '/' || message.charAt(0) == '!') {
|
if (targetUid != GameConstants.SERVER_CONSOLE_UID || message.charAt(0) == '/' || message.charAt(0) == '!') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 否则执行玩家任意消息方法
|
||||||
handlePlayerMessage(player, message);
|
handlePlayerMessage(player, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理玩家消息
|
* 处理玩家消息
|
||||||
* @param player 玩家对象
|
*
|
||||||
|
* @param player 玩家对象
|
||||||
* @param message 消息内容
|
* @param message 消息内容
|
||||||
*/
|
*/
|
||||||
private void handlePlayerMessage(Player player, String message) {
|
private void handlePlayerMessage(Player player, String message) {
|
||||||
@ -85,8 +155,8 @@ public class OpenChatSystem extends ChatSystem {
|
|||||||
|
|
||||||
// 检测是否正在禁言中
|
// 检测是否正在禁言中
|
||||||
if (checkIsBanning(player)) {
|
if (checkIsBanning(player)) {
|
||||||
plugin.getLogger().warn(String.format("Message blocked (banning): player=%s(%d): \"%s\"",
|
logger.warn("Message blocked (banning): player={}({}): \"{}\"",
|
||||||
player.getNickname(), player.getUid(), message));
|
player.getNickname(), player.getUid(), message);
|
||||||
if (!plugin.getConfig().bannedFeedback.isEmpty()) {
|
if (!plugin.getConfig().bannedFeedback.isEmpty()) {
|
||||||
player.dropMessage(plugin.getConfig().bannedFeedback);
|
player.dropMessage(plugin.getConfig().bannedFeedback);
|
||||||
}
|
}
|
||||||
@ -96,8 +166,8 @@ public class OpenChatSystem extends ChatSystem {
|
|||||||
// 处理发言频率限制
|
// 处理发言频率限制
|
||||||
if (!checkMessageFre(player)) {
|
if (!checkMessageFre(player)) {
|
||||||
// 可提示也可忽略,忽略可让玩家以为自己发送成功,其实别人看不到
|
// 可提示也可忽略,忽略可让玩家以为自己发送成功,其实别人看不到
|
||||||
plugin.getLogger().warn(String.format("Message blocked (too often): player=%s(%d): \"%s\"",
|
logger.warn("Message blocked (too often): player={}({}): \"{}\"",
|
||||||
player.getNickname(), player.getUid(), message));
|
player.getNickname(), player.getUid(), message);
|
||||||
if (!plugin.getConfig().msgTooFrequentFeedback.isEmpty()) {
|
if (!plugin.getConfig().msgTooFrequentFeedback.isEmpty()) {
|
||||||
player.dropMessage(
|
player.dropMessage(
|
||||||
plugin.getConfig().msgTooFrequentFeedback
|
plugin.getConfig().msgTooFrequentFeedback
|
||||||
@ -108,25 +178,80 @@ public class OpenChatSystem extends ChatSystem {
|
|||||||
|
|
||||||
// 处理发言内容审查
|
// 处理发言内容审查
|
||||||
if (!checkMessageModeration(message)) {
|
if (!checkMessageModeration(message)) {
|
||||||
plugin.getLogger().warn(String.format("Message blocked (moderation): player=%s(%d): \"%s\"",
|
logger.warn("Message blocked (moderation): player={}({}): \"{}\"",
|
||||||
player.getNickname(), player.getUid(), message));
|
player.getNickname(), player.getUid(), message);
|
||||||
return;
|
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("{nickName}", player.getNickname())
|
||||||
.replace("{uid}", String.valueOf(player.getUid()))
|
.replace("{uid}", String.valueOf(player.getUid()))
|
||||||
.replace("{message}", message);
|
.replace("{message}", message);
|
||||||
|
|
||||||
// 转发给其它玩家
|
// 转发给其它玩家
|
||||||
for (Player p : getServer().getPlayers().values()) {
|
for (Player p : getServer().getPlayers().values()) {
|
||||||
// 将消息发送给除了自己以外所有未关闭聊天的玩家
|
// 将消息发送给除了自己以外所有未关闭聊天的玩家
|
||||||
if (p != player && !plugin.getData().offChatPlayers.contains(p.getUid())) {
|
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);
|
p.dropMessage(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查玩家是否正在禁言中
|
||||||
|
*
|
||||||
|
* @param player 玩家对象
|
||||||
|
* @return 是否禁言中
|
||||||
|
*/
|
||||||
private boolean checkIsBanning(Player player) {
|
private boolean checkIsBanning(Player player) {
|
||||||
var banList = plugin.getData().banList;
|
var banList = plugin.getData().banList;
|
||||||
// 检测是否正在禁言中
|
// 检测是否正在禁言中
|
||||||
@ -142,6 +267,7 @@ public class OpenChatSystem extends ChatSystem {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息内容审查
|
* 消息内容审查
|
||||||
|
*
|
||||||
* @param message 消息
|
* @param message 消息
|
||||||
* @return 是否合法合规
|
* @return 是否合法合规
|
||||||
*/
|
*/
|
||||||
@ -150,10 +276,16 @@ public class OpenChatSystem extends ChatSystem {
|
|||||||
return !message.isEmpty();
|
return !message.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// region 发言频率限制
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发言频率计时器
|
||||||
|
*/
|
||||||
Int2ObjectMap<LongArrayFIFOQueue> speakingTimes = new Int2ObjectOpenHashMap<>();
|
Int2ObjectMap<LongArrayFIFOQueue> speakingTimes = new Int2ObjectOpenHashMap<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息频率检查
|
* 消息频率检查
|
||||||
|
*
|
||||||
* @param player 玩家对象
|
* @param player 玩家对象
|
||||||
* @return 是否在约定阈值内
|
* @return 是否在约定阈值内
|
||||||
*/
|
*/
|
||||||
@ -166,4 +298,6 @@ public class OpenChatSystem extends ChatSystem {
|
|||||||
list.dequeueLong();
|
list.dequeueLong();
|
||||||
return list.size() <= plugin.getConfig().messageFreLimitPerMinute;
|
return list.size() <= plugin.getConfig().messageFreLimitPerMinute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,14 @@ import java.util.List;
|
|||||||
|
|
||||||
@Command(label = "serverchat",
|
@Command(label = "serverchat",
|
||||||
aliases = { "sc" },
|
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",
|
permission = "server.chat",
|
||||||
permissionTargeted = "server.chat.others",
|
permissionTargeted = "server.chat.others",
|
||||||
targetRequirement = Command.TargetRequirement.NONE)
|
targetRequirement = Command.TargetRequirement.NONE)
|
||||||
@ -98,6 +105,18 @@ public class ChatServerCommands implements CommandHandler {
|
|||||||
plugin.loadConfig();
|
plugin.loadConfig();
|
||||||
CommandHandler.sendMessage(sender, "OK");
|
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);
|
default -> sendUsageMessage(sender);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user