mirror of
https://github.com/jie65535/gc-openchat-plugin.git
synced 2025-12-15 18:31:33 +08:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 01a2feec03 | |||
| 04b8d2dc77 | |||
| 4129eaa13e | |||
| dc90a17824 | |||
| e795c5a9c4 | |||
| 28456bb179 | |||
| 397cf392f5 | |||
| d9ef6b3097 | |||
| 5923f2c0eb | |||
| 443f04f910 | |||
| 36ede5e634 | |||
| 61b9be9888 | |||
| 2a696b4854 | |||
| b119bb0cb9 | |||
| d8caa42899 | |||
|
|
d060b1804f |
3
.idea/misc.xml
generated
3
.idea/misc.xml
generated
@@ -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>
|
||||
|
||||
189
README-en-US.md
189
README-en-US.md
@@ -6,7 +6,7 @@
|
||||
[](https://github.com/jie65535/gc-openchat-plugin/releases/latest)
|
||||
[](https://github.com/jie65535/gc-openchat-plugin/actions/workflows/build.yml)
|
||||
|
||||
[中文](README.md) | English
|
||||
[简中](README.md) | [繁中](README-zh-TW.md) | EN | [RU](README-ru-RU.md)
|
||||
|
||||
Chat with players in the server
|
||||
|
||||
@@ -18,9 +18,11 @@ Talking to the server account in the game is equivalent to sending to the world
|
||||
- [x] Chat between players
|
||||
- [x] Chat management commands
|
||||
- [x] Speaking Frequency Limit
|
||||
- [ ] Chat Moderation
|
||||
- [ ] Chat api _(~~OneBot api~~)_
|
||||
- [ ] ...
|
||||
- [x] Chat api (OneBot)
|
||||
- [x] Player on/offline broadcast
|
||||
- [x] Chat Moderation
|
||||
- [x] op execution commands
|
||||
- [ ] Chat api (Minimal)
|
||||
|
||||
## Install
|
||||
|
||||
@@ -39,9 +41,85 @@ Server command (requires `server.chat.others` permissions) :
|
||||
- `/serverchat unban|unmute @uid` Unmute a specified player
|
||||
- `/serverchat limit <timesPerMinute>` Set a frequency limit for sending messages
|
||||
- `/serverchat reload` reload config.json
|
||||
- `/serverchat group <groupId>` Set the connected group id
|
||||
- `/serverchat op|deop <userId(QQ)>` Set or remove op
|
||||
- The account set as op can directly execute commands with the admin prefix in the specified group
|
||||
- The command prefix can be set in the configuration file `adminPrefix`, the default is `/`, for example `/sc ban @10002`
|
||||
|
||||
`/serverchat` can be aliased as `/sc`
|
||||
|
||||
## Bot Connect
|
||||

|
||||
|
||||
### Connect | Bot -> OpenChat
|
||||
1. Create a WebSocket Client
|
||||
2. Add header `Authorization: Bearer **token**` for authentication connection
|
||||
3. Add header `X-Self-ID: 123456` to identify the connected bot account (optional)
|
||||
4. Connect to `/openchat`, for example `ws://127.0.0.1:443/openchat`
|
||||
|
||||
### Message ([OneBot-v11](https://github.com/botuniverse/onebot-11))
|
||||
Only the fields used by the plugin are listed below
|
||||
#### Group message | Bot -> OpenChat
|
||||
```json
|
||||
{
|
||||
"post_type": "message",
|
||||
"message_type": "group",
|
||||
"sub_type": "normal",
|
||||
"group_id": 123456,
|
||||
"raw_message": "Plain Message",
|
||||
"sender": {
|
||||
"role": "member",
|
||||
"level": "71",
|
||||
"user_id": 123456789,
|
||||
"nickname": "NickName",
|
||||
"title": "UserTitle",
|
||||
"card": "UserCard"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Group message | OpenChat -> Bot
|
||||
```json
|
||||
{
|
||||
"action": "send_group_msg",
|
||||
"params": {
|
||||
"group_id": 123456,
|
||||
"message": "Plain Message",
|
||||
"auto_escape": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Channel message | Bot -> OpenChat
|
||||
```json
|
||||
{
|
||||
"post_type": "message",
|
||||
"message_type": "group",
|
||||
"sub_type": "normal",
|
||||
"guildId": "123456",
|
||||
"channel_id": "1234",
|
||||
"user_id": "123456789",
|
||||
"message": "Plain Message",
|
||||
"sender": {
|
||||
"user_id": 123456789,
|
||||
"nickname": "NickName"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Channel message | OpenChat -> Bot
|
||||
```json
|
||||
{
|
||||
"action": "send_guild_channel_msg",
|
||||
"params": {
|
||||
"guild_id": "123456",
|
||||
"channel_id": "1234",
|
||||
"message": "Plain Message",
|
||||
"auto_escape": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Config
|
||||
```json5
|
||||
{
|
||||
@@ -63,7 +141,108 @@ Server command (requires `server.chat.others` permissions) :
|
||||
|
||||
// Message too frequent feedback message
|
||||
// {limit} messageFreLimitPerMinute
|
||||
msgTooFrequentFeedback: "服务器设置每分钟仅允许发言{limit}次"
|
||||
msgTooFrequentFeedback: "服务器设置每分钟仅允许发言{limit}次",
|
||||
|
||||
// Whether to log the chat
|
||||
"logChat": true,
|
||||
|
||||
// WebSocket Access Token
|
||||
// security token, only authorized connections are allowed
|
||||
// If it is empty, a 32-bit random token will be automatically generated at startup and displayed on the console
|
||||
"wsToken": "",
|
||||
|
||||
// WebSocket Path
|
||||
// The reverse WS path, that is, the bot connect to the WS interface path opened by this plug-in
|
||||
// If you don't want to open WS, leave it empty, the default is "/openchat"
|
||||
// OneBot setting example: ws://127.0.0.1:443/openchat
|
||||
"wsPath": "/openchat",
|
||||
|
||||
// // WebSocket Address
|
||||
// // Forward WS address, that is, the WS interface address opened by this plug-in to actively connect to the bot
|
||||
// // Example: ws://127.0.0.1:8080
|
||||
// // Leave blank if not required
|
||||
// // TODO: Due to the need to introduce external dependencies, the forward WS method is not implemented yet
|
||||
// public String wsAddress: "",
|
||||
|
||||
// Group id
|
||||
// You can use the command `/sc group <groupId>` to set
|
||||
"groupId": 0,
|
||||
|
||||
// Group to Game format
|
||||
// {id} User id
|
||||
// {name} Card or Nickname
|
||||
// {message} message
|
||||
"groupToGameFormat": "<color=#6699CC>[QQ]</color><color=#99CC99>{name}</color>: {message}",
|
||||
|
||||
// Game to Group format
|
||||
// {nickName} Player Nick name
|
||||
// {uid} Player Uid
|
||||
// {message} message
|
||||
"gameToGroupFormat": "[GC]{nickName}({uid}): {message}",
|
||||
|
||||
// /**
|
||||
// * Guild id
|
||||
// */
|
||||
// public String guildId: "",
|
||||
// /**
|
||||
// * Channel ids
|
||||
// */
|
||||
// public List<String> channelIds: new ArrayList<>(),
|
||||
|
||||
// Enable forwarding in-game chat to group chat
|
||||
"isSendToBot": true,
|
||||
|
||||
// Enable receiving group messages and send them to the game
|
||||
"isSendToGame": true,
|
||||
|
||||
// Admin id
|
||||
"adminIds": [0],
|
||||
|
||||
// Admin command prefix
|
||||
"adminPrefix": "/",
|
||||
|
||||
// Is enable login message to bot
|
||||
"sendLoginMessageToBot": true,
|
||||
|
||||
// Login format
|
||||
// {nickName} Player Nick name
|
||||
// {uid} Player Uid
|
||||
"loginMessageFormat": "{nickName}({uid}) 加入了服务器",
|
||||
|
||||
// Is enable login message to game
|
||||
"sendLoginMessageToGame": true,
|
||||
|
||||
// Player login message format in game
|
||||
// {nickName} Player Nick name
|
||||
// {uid} Player Uid
|
||||
"loginMessageFormatInGame": "<color=#99CC99>{nickName}({uid}) 加入了游戏</color>",
|
||||
|
||||
// Is enable logout message to bot
|
||||
"sendLogoutMessageToBot": true,
|
||||
|
||||
// Player logout message format
|
||||
// {nickName} Player Nick name
|
||||
// {uid} Player Uid
|
||||
"logoutMessageFormat": "{nickName}({uid}) 离开了服务器",
|
||||
|
||||
// Is enable logout message to game
|
||||
"sendLogoutMessageToGame": true,
|
||||
|
||||
// Logout message format in game
|
||||
// {nickName} Player Nick name
|
||||
// {uid} Player Uid
|
||||
"logoutMessageFormatInGame": "<color=#99CC99>{nickName}({uid}) 离开了游戏</color>",
|
||||
}
|
||||
```
|
||||
|
||||
## Sensitive word filtering system
|
||||
At present, the most basic sensitive word filtering function has been implemented, and a streamlined sensitive word library is attached.
|
||||
The thesaurus will be released to the plug-in data directory when it is first started.
|
||||
|
||||
The file name is `SensitiveWordList.txt`, and each line contains a sensitive word. You can maintain this file yourself, and you can use `/sc reload` to read it again after modification.
|
||||
|
||||
When sensitive words are detected in the in-game player chat, it will not be forwarded and will be printed in the console.
|
||||
|
||||
There is currently no penalty mechanism set up, it's just that it's sent out and others can't see it, and I don't know that it hasn't been sent out.
|
||||
|
||||
If you have better suggestions, welcome [submit issue](https://github.com/jie65535/gc-openchat-plugin/issues/new)
|
||||
75
README-ru-RU.md
Normal file
75
README-ru-RU.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# gc-openchat-plugin
|
||||
|
||||
[](https://github.com/jie65535/gc-openchat-plugin/blob/main/LICENSE)
|
||||
[](https://github.com/jie65535/gc-openchat-plugin/stargazers)
|
||||
[](https://github.com/jie65535/gc-openchat-plugin/releases)
|
||||
[](https://github.com/jie65535/gc-openchat-plugin/releases/latest)
|
||||
[](https://github.com/jie65535/gc-openchat-plugin/actions/workflows/build.yml)
|
||||
|
||||
[简中](README.md) | [繁中](README-zh-TW.md) | [EN](README-en-US.md) | RU
|
||||
|
||||
Разрешить игрокам общаться внутри сервера
|
||||
|
||||

|
||||
|
||||
Разговор с учетной записью сервера в игре эквивалентен отправке на мировой канал, и все игроки на сервере могут получить сообщение.
|
||||
|
||||
## TODO List
|
||||
- [x] Чат между игроками
|
||||
- [x] Команды управления чатом
|
||||
- [x] Ограничение скорости речи
|
||||
- [ ] Модерация контента чата
|
||||
- [ ] API чата _(~~OneBot api~~)_
|
||||
- [ ] ...
|
||||
|
||||
## Установить
|
||||
|
||||
1. Загрузите `jar` из [Release](https://github.com/jie65535/gc-openchat-plugin/releases)
|
||||
2. Поместите его в папку `plugins`
|
||||
|
||||
## Порядок
|
||||
Игроки используют:
|
||||
- `/chat on` принимать сообщения чата (по умолчанию)
|
||||
- `/chat off` отключает сообщения чата
|
||||
|
||||
Для управления (требуется разрешение `server.chat.others`):
|
||||
- `/serverchat on` включить серверный чат (по умолчанию)
|
||||
- `/serverchat off` отключить серверный чат
|
||||
- `/serverchat ban @uid [время (минуты)]` забанить определенных игроков
|
||||
- `/serverchat unban @uid` Разблокировать указанного игрока
|
||||
- `/serverchat limit <количество раз в минуту>` установить ограничение частоты отправки сообщений
|
||||
- `/serverchat reload` перезагрузить файл конфигурации
|
||||
|
||||
`/serverchat` может иметь псевдоним `/sc`
|
||||
|
||||
|
||||
## конфигурация
|
||||
```json5
|
||||
{
|
||||
// переключение чата на сервере
|
||||
serverChatEnabled: true,
|
||||
|
||||
// формат сообщения серверного чата
|
||||
// {nickName} никнейм игрока
|
||||
// {uid} это UID игрока
|
||||
// {message} это содержимое сообщения
|
||||
serverChatFormat: "<color=#99CC99>{nickName}({uid})</color>: {message}",
|
||||
|
||||
// Ограничить количество говорящих сообщений в минуту
|
||||
messageFreLimitPerMinute: 20,
|
||||
|
||||
// Отправлять ли сообщение, когда игрок присоединяется
|
||||
sendJoinMessage: true,
|
||||
|
||||
// Отправляем сообщение, когда игрок присоединяется
|
||||
joinMessage: "本服已启用聊天,/chat on 开启(默认),/chat off 屏蔽",
|
||||
|
||||
// Запрещенное сообщение обратной связи
|
||||
bannedFeedback: "你已经被禁言!",
|
||||
|
||||
// Сообщение обратной связи слишком часто
|
||||
// {limit} Максимальное время, установленное сервером
|
||||
msgTooFrequentFeedback: "服务器设置每分钟仅允许发言{limit}次"
|
||||
}
|
||||
```
|
||||
|
||||
75
README-zh-TW.md
Normal file
75
README-zh-TW.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# gc-openchat-plugin
|
||||
|
||||
[](https://github.com/jie65535/gc-openchat-plugin/blob/main/LICENSE)
|
||||
[](https://github.com/jie65535/gc-openchat-plugin/stargazers)
|
||||
[](https://github.com/jie65535/gc-openchat-plugin/releases)
|
||||
[](https://github.com/jie65535/gc-openchat-plugin/releases/latest)
|
||||
[](https://github.com/jie65535/gc-openchat-plugin/actions/workflows/build.yml)
|
||||
|
||||
[简中](README.md) | 繁中 | [EN](README-en-US.md) | [RU](README-ru-RU.md)
|
||||
|
||||
讓玩家在服務器內聊天
|
||||
|
||||

|
||||
|
||||
在遊戲內與服務器賬號對話,相當於發送到世界頻道,服務器內所有玩家均可收到消息。
|
||||
|
||||
## TODO List
|
||||
- [x] 玩家間聊天
|
||||
- [x] 聊天管理命令
|
||||
- [x] 發言頻率限制
|
||||
- [ ] 聊天內容審查
|
||||
- [ ] 聊天api _(~~OneBot api~~)_
|
||||
- [ ] ...
|
||||
|
||||
## 安裝
|
||||
|
||||
1. 在 [Release](https://github.com/jie65535/gc-openchat-plugin/releases) 下載`jar`
|
||||
2. 放入 `plugins` 文件夾即可
|
||||
|
||||
## 命令
|
||||
玩家用:
|
||||
- `/chat on` 接受聊天消息(默認)
|
||||
- `/chat off` 屏蔽聊天消息
|
||||
|
||||
管理用(需要 `server.chat.others` 權限):
|
||||
- `/serverchat on` 啟用服務器聊天(默認)
|
||||
- `/serverchat off` 關閉服務器聊天
|
||||
- `/serverchat ban @uid [時間(分鐘)]` 禁言指定玩家
|
||||
- `/serverchat unban @uid` 解除指定玩家禁言
|
||||
- `/serverchat limit <次每分钟>` 設置發消息頻率限制
|
||||
- `/serverchat reload` 重載配置文件
|
||||
|
||||
`/serverchat` 可用别名 `/sc`
|
||||
|
||||
|
||||
## 配置
|
||||
```json5
|
||||
{
|
||||
// 服務器聊天開關
|
||||
serverChatEnabled: true,
|
||||
|
||||
// 服務器聊天消息格式
|
||||
// {nickName} 為玩家暱稱
|
||||
// {uid} 為玩家UID
|
||||
// {message} 為消息內容
|
||||
serverChatFormat: "<color=#99CC99>{nickName}({uid})</color>: {message}",
|
||||
|
||||
// 每分鐘發言消息數限制
|
||||
messageFreLimitPerMinute: 20,
|
||||
|
||||
// 是否在玩家加入時發送消息
|
||||
sendJoinMessage: true,
|
||||
|
||||
// 玩家加入時發送消息
|
||||
joinMessage: "本服已启用聊天,/chat on 开启(默认),/chat off 屏蔽",
|
||||
|
||||
// 被禁言反饋消息
|
||||
bannedFeedback: "你已经被禁言!",
|
||||
|
||||
// 消息太頻繁反饋消息
|
||||
// {limit} 服務器設置的限制次數
|
||||
msgTooFrequentFeedback: "服务器设置每分钟仅允许发言{limit}次"
|
||||
}
|
||||
```
|
||||
|
||||
240
README.md
240
README.md
@@ -6,7 +6,7 @@
|
||||
[](https://github.com/jie65535/gc-openchat-plugin/releases/latest)
|
||||
[](https://github.com/jie65535/gc-openchat-plugin/actions/workflows/build.yml)
|
||||
|
||||
中文 | [English](README-en-US.md)
|
||||
简中 | [繁中](README-zh-TW.md) | [EN](README-en-US.md) | [RU](README-ru-RU.md)
|
||||
|
||||
让玩家在服务器内聊天
|
||||
|
||||
@@ -14,13 +14,15 @@
|
||||
|
||||
在游戏内与服务器账号对话,相当于发送到世界频道,服务器内所有玩家均可收到消息。
|
||||
|
||||
## TODO List
|
||||
## 功能列表
|
||||
- [x] 玩家间聊天
|
||||
- [x] 聊天管理命令
|
||||
- [x] 发言频率限制
|
||||
- [ ] 聊天内容审查
|
||||
- [ ] 聊天api _(~~OneBot api~~)_
|
||||
- [ ] ...
|
||||
- [x] 聊天api (OneBot)
|
||||
- [x] 玩家上下线提醒
|
||||
- [x] 指定管理账号在群内执行控制台命令
|
||||
- [x] 聊天内容审查
|
||||
- [ ] 聊天api (Minimal)
|
||||
|
||||
## 安装
|
||||
|
||||
@@ -39,37 +41,247 @@
|
||||
- `/serverchat unban @uid` 解除指定玩家禁言
|
||||
- `/serverchat limit <次每分钟>` 设置发消息频率限制
|
||||
- `/serverchat reload` 重载配置文件
|
||||
- `/serverchat group <groupId>` 设置互联群号
|
||||
- `/serverchat op|deop <userId(QQ)>` 设置或解除管理员
|
||||
- 设置为管理的账号可以在指定群内直接用管理前缀执行命令
|
||||
- 命令前缀可在配置文件中设置 `adminPrefix` ,默认为 `/`,例 `/sc ban @10002`
|
||||
- 目前在群内执行命令暂时没有回复,因为控制台执行过程只会log到控制台,不好捕获
|
||||
|
||||
`/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` )
|
||||
|
||||
### 群服互联参考流程
|
||||
1. 装好插件启动后,记录下首次生成的 `Token`,或者自己填写一个 `Token`
|
||||
2. 下载 [go-cqhttp](https://github.com/Mrs4s/go-cqhttp) 并初始化配置,打开配置文件 `config.yml`
|
||||
3. 在 `access-token: ''` 填写前面所述的 `Token` 内容
|
||||
4. 在 `ws-reverse` 选项下的 `universal` 填写GC的服务器地址加路径,例如 `ws://127.0.0.1:443/openchat`
|
||||
5. 配置你的Bot账号和登录协议,建议使用 `Android Watch` 登录。具体参考文档 [配置](https://docs.go-cqhttp.org/guide/config.html)。(在 `device.json` 中 `"protocol": 5` 修改为 `"protocol": 2` )
|
||||
6. 在GC中使用 `/sc group <groupId>` 来设置要互联的群聊
|
||||
7. 在GC中使用 `/sc op <userId(QQ)>` 来设置管理员账号
|
||||
8. 现在,理论上已经完成了群服互联,在群里可以看到玩家上下线和聊天,同时玩家也可以在游戏里看到群里聊天,
|
||||
你还可以在群里用默认前缀 `/` 来执行命令,
|
||||
|
||||
|
||||
## 配置
|
||||
_值得注意的是,本插件支持的是 [OneBot-v11](https://github.com/botuniverse/onebot-11) 协议,理论上所有支持OneBot-v11 [反向WebSocket](https://github.com/botuniverse/onebot-11/blob/master/communication/ws-reverse.md) 的机器人框架都可以连接,不仅限于cqhttp。_
|
||||
|
||||
|
||||
---
|
||||
|
||||
## OpenChat 对接指南
|
||||
|
||||
### 连接 | Bot -> OpenChat
|
||||
1. 创建 WebSocket Client
|
||||
2. 添加请求头 `Authorization: Bearer **token**` 用于认证连接
|
||||
3. 添加请求头 `X-Self-ID: 123456` 用来标识连接的bot账号 (可选)
|
||||
4. 连接到 `/openchat`,例如 `ws://127.0.0.1:443/openchat`
|
||||
|
||||
### 通讯报文
|
||||
以下仅列出插件需要的字段
|
||||
#### 群消息 | Bot -> OpenChat
|
||||
```json
|
||||
{
|
||||
"post_type": "message",
|
||||
"message_type": "group",
|
||||
"sub_type": "normal",
|
||||
"group_id": 123456,
|
||||
"raw_message": "Plain Message",
|
||||
"sender": {
|
||||
"role": "member",
|
||||
"level": "71",
|
||||
"user_id": 123456789,
|
||||
"nickname": "NickName",
|
||||
"title": "UserTitle",
|
||||
"card": "UserCard"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 群消息 | OpenChat -> Bot
|
||||
```json
|
||||
{
|
||||
"action": "send_group_msg",
|
||||
"params": {
|
||||
"group_id": 123456,
|
||||
"message": "Plain Message",
|
||||
"auto_escape": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 频道消息 | Bot -> OpenChat
|
||||
```json
|
||||
{
|
||||
"post_type": "message",
|
||||
"message_type": "group",
|
||||
"sub_type": "normal",
|
||||
"guildId": "123456",
|
||||
"channel_id": "1234",
|
||||
"user_id": "123456789",
|
||||
"message": "Plain Message",
|
||||
"sender": {
|
||||
"user_id": 123456789,
|
||||
"nickname": "NickName"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 频道消息 | OpenChat -> Bot
|
||||
```json
|
||||
{
|
||||
"action": "send_guild_channel_msg",
|
||||
"params": {
|
||||
"guild_id": "123456",
|
||||
"channel_id": "1234",
|
||||
"message": "Plain Message",
|
||||
"auto_escape": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 插件配置
|
||||
```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,
|
||||
|
||||
// 管理员账号
|
||||
"adminIds": [0],
|
||||
|
||||
// 管理员执行命令前缀
|
||||
"adminPrefix": "/",
|
||||
|
||||
// 是否启用登录消息
|
||||
// 当玩家登录服务器时,发送消息通知到群里
|
||||
"sendLoginMessageToBot": true,
|
||||
|
||||
// 玩家登录服务器消息格式
|
||||
// {nickName} 为玩家昵称
|
||||
// {uid} 为玩家UID
|
||||
"loginMessageFormat": "{nickName}({uid}) 加入了服务器",
|
||||
|
||||
// 是否启用登录消息
|
||||
// 当玩家登录服务器时,发送消息通知到游戏里
|
||||
"sendLoginMessageToGame": true,
|
||||
|
||||
// 玩家登录服务器消息格式
|
||||
// {nickName} 为玩家昵称
|
||||
// {uid} 为玩家UID
|
||||
"loginMessageFormatInGame": "<color=#99CC99>{nickName}({uid}) 加入了游戏</color>",
|
||||
|
||||
// 是否启用登出消息
|
||||
// 当玩家离开服务器时,发送消息通知到群里
|
||||
"sendLogoutMessageToBot": true,
|
||||
|
||||
// 玩家登出服务器消息格式
|
||||
// {nickName} 为玩家昵称
|
||||
// {uid} 为玩家UID
|
||||
"logoutMessageFormat": "{nickName}({uid}) 离开了服务器",
|
||||
|
||||
// 是否启用登出消息
|
||||
// 当玩家离开服务器时,发送消息通知到群里
|
||||
"sendLogoutMessageToGame": true,
|
||||
|
||||
// 玩家登出服务器消息格式
|
||||
// {nickName} 为玩家昵称
|
||||
// {uid} 为玩家UID
|
||||
"logoutMessageFormatInGame": "<color=#99CC99>{nickName}({uid}) 离开了游戏</color>",
|
||||
}
|
||||
```
|
||||
|
||||
## 敏感词过滤系统
|
||||
目前实现了一个最基础的敏感词过滤功能, 并附带了一个精简的敏感词库,
|
||||
在首次启动时会把词库释放到插件数据目录下。
|
||||
|
||||
文件名叫 `SensitiveWordList.txt`,每行包含一个敏感词,你可以自己维护这个文件,修改后可以用 `/sc reload` 重新读取。
|
||||
|
||||
当检测到游戏内玩家聊天中包含敏感词,将不会进行转发,并且会在控制台中打印。
|
||||
|
||||
目前暂未设定惩罚机制,仅仅只是发出去别人看不到,自己不知道这个没发出去。
|
||||
|
||||
如果你有更好的建议,欢迎[提交 issue](https://github.com/jie65535/gc-openchat-plugin/issues/new)
|
||||
@@ -3,7 +3,7 @@ plugins {
|
||||
}
|
||||
|
||||
group 'com.github.jie65535.openchat'
|
||||
version 'dev-0.1.0'
|
||||
version '1.0.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: 550 KiB |
301
src/main/java/com/github/jie65535/minionebot/MiniOneBot.java
Normal file
301
src/main/java/com/github/jie65535/minionebot/MiniOneBot.java
Normal file
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
* 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 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新OneBot Token
|
||||
* @param token 授权令牌
|
||||
*/
|
||||
public void setToken(String token) {
|
||||
if (!Objects.equals(this.token, token)) {
|
||||
this.token = token;
|
||||
if (server != null)
|
||||
server.setToken(token);
|
||||
// if (client != null)
|
||||
// client.setToken(token);
|
||||
logger.info("MiniOneBot Token changed.");
|
||||
}
|
||||
}
|
||||
|
||||
// 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,127 @@
|
||||
/*
|
||||
* 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 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 setToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
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,42 @@
|
||||
*/
|
||||
package com.github.jie65535.openchat;
|
||||
|
||||
//import emu.grasscutter.server.event.player.PlayerJoinEvent;
|
||||
//import emu.grasscutter.server.scheduler.ServerTaskScheduler;
|
||||
import emu.grasscutter.command.CommandMap;
|
||||
import emu.grasscutter.server.event.game.ReceiveCommandFeedbackEvent;
|
||||
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 final class EventListeners {
|
||||
public static void onJoin(PlayerJoinEvent event) {
|
||||
var cs = OpenChatPlugin.getInstance().getServer().getChatSystem();
|
||||
if (cs instanceof OpenChatSystem) {
|
||||
((OpenChatSystem) cs).onPlayerJoin(event);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
// }
|
||||
//}
|
||||
|
||||
private static final StringBuilder consoleMessageHandler = new StringBuilder();
|
||||
private static StringBuilder commandResponseHandler;
|
||||
public static String runConsoleCommand(String rawCommand) {
|
||||
synchronized (consoleMessageHandler) {
|
||||
commandResponseHandler = consoleMessageHandler;
|
||||
consoleMessageHandler.setLength(0);
|
||||
// 尝试执行管理员命令
|
||||
CommandMap.getInstance().invoke(null, null, rawCommand);
|
||||
commandResponseHandler = null;
|
||||
return consoleMessageHandler.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 命令执行反馈事件处理
|
||||
*/
|
||||
public static void onCommandResponse(ReceiveCommandFeedbackEvent event) {
|
||||
if (commandResponseHandler == null || event.getPlayer() != null) return;
|
||||
|
||||
if (!consoleMessageHandler.isEmpty()) {
|
||||
// New line
|
||||
consoleMessageHandler.append(System.lineSeparator());
|
||||
}
|
||||
consoleMessageHandler.append(event.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
*/
|
||||
package com.github.jie65535.openchat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class OpenChatConfig {
|
||||
|
||||
/**
|
||||
@@ -57,4 +60,136 @@ 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 ArrayList<Long> adminIds = new ArrayList<>(List.of(0L));
|
||||
|
||||
/**
|
||||
* 管理员执行命令前缀
|
||||
*/
|
||||
public String adminPrefix = "/";
|
||||
|
||||
/**
|
||||
* 是否启用登录消息
|
||||
* 当玩家登录服务器时,发送消息通知到群里
|
||||
*/
|
||||
public boolean sendLoginMessageToBot = true;
|
||||
|
||||
/**
|
||||
* 玩家登录服务器消息格式
|
||||
* {nickName} 为玩家昵称
|
||||
* {uid} 为玩家UID
|
||||
*/
|
||||
public String loginMessageFormat = "{nickName}({uid}) 加入了服务器";
|
||||
|
||||
/**
|
||||
* 是否启用登录消息
|
||||
* 当玩家登录服务器时,发送消息通知到游戏里
|
||||
*/
|
||||
public boolean sendLoginMessageToGame = true;
|
||||
|
||||
/**
|
||||
* 玩家登录服务器消息格式(游戏内)
|
||||
* {nickName} 为玩家昵称
|
||||
* {uid} 为玩家UID
|
||||
*/
|
||||
public String loginMessageFormatInGame = "<color=#99CC99>{nickName}({uid}) 加入了游戏</color>";
|
||||
|
||||
/**
|
||||
* 是否启用登出消息
|
||||
* 当玩家离开服务器时,发送消息通知到群里
|
||||
*/
|
||||
public boolean sendLogoutMessageToBot = true;
|
||||
|
||||
/**
|
||||
* 玩家登出服务器消息格式
|
||||
* {nickName} 为玩家昵称
|
||||
* {uid} 为玩家UID
|
||||
*/
|
||||
public String logoutMessageFormat = "{nickName}({uid}) 离开了服务器";
|
||||
|
||||
/**
|
||||
* 是否启用登出消息
|
||||
* 当玩家登录服务器时,发送消息通知到游戏里
|
||||
*/
|
||||
public boolean sendLogoutMessageToGame = true;
|
||||
|
||||
/**
|
||||
* 玩家登出服务器消息格式(游戏内)
|
||||
* {nickName} 为玩家昵称
|
||||
* {uid} 为玩家UID
|
||||
*/
|
||||
public String logoutMessageFormatInGame = "<color=#99CC99>{nickName}({uid}) 离开了游戏</color>";
|
||||
}
|
||||
|
||||
@@ -19,13 +19,18 @@ package com.github.jie65535.openchat;
|
||||
|
||||
import com.github.jie65535.openchat.commands.ChatPlayerCommands;
|
||||
import com.github.jie65535.openchat.commands.ChatServerCommands;
|
||||
import com.github.jie65535.openchat.utils.SensitiveWordFilter;
|
||||
import emu.grasscutter.plugin.Plugin;
|
||||
import emu.grasscutter.server.event.EventHandler;
|
||||
import emu.grasscutter.server.event.HandlerPriority;
|
||||
import emu.grasscutter.server.event.game.ReceiveCommandFeedbackEvent;
|
||||
import emu.grasscutter.server.event.player.PlayerJoinEvent;
|
||||
import emu.grasscutter.utils.JsonUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class OpenChatPlugin extends Plugin {
|
||||
private static OpenChatPlugin instance;
|
||||
@@ -33,6 +38,54 @@ public final class OpenChatPlugin extends Plugin {
|
||||
return instance;
|
||||
}
|
||||
|
||||
private OpenChatSystem openChatSystem;
|
||||
public OpenChatSystem getOpenChatSystem() {
|
||||
return openChatSystem;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
instance = this;
|
||||
loadConfig();
|
||||
loadData();
|
||||
loadSensitiveWordList();
|
||||
getLogger().info("[OpenChat] Loaded.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
// Register event listeners.
|
||||
new EventHandler<>(PlayerJoinEvent.class)
|
||||
.priority(HandlerPriority.NORMAL)
|
||||
.listener(EventListeners::onJoin)
|
||||
.register(this);
|
||||
new EventHandler<>(ReceiveCommandFeedbackEvent.class)
|
||||
.priority(HandlerPriority.NORMAL)
|
||||
.listener(EventListeners::onCommandResponse)
|
||||
.register(this);
|
||||
|
||||
// Register commands.
|
||||
getHandle().registerCommand(new ChatServerCommands());
|
||||
getHandle().registerCommand(new ChatPlayerCommands());
|
||||
|
||||
// Set my chat system.
|
||||
openChatSystem = new OpenChatSystem(this);
|
||||
getServer().setChatSystem(openChatSystem);
|
||||
|
||||
// Log a plugin status message.
|
||||
getLogger().info("[OpenChat] Enabled, see https://github.com/jie65535/gc-openchat-plugin");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
saveData();
|
||||
saveConfig();
|
||||
getLogger().info("[OpenChat] Disabled.");
|
||||
}
|
||||
|
||||
|
||||
// region config
|
||||
|
||||
private OpenChatConfig config;
|
||||
public OpenChatConfig getConfig() {
|
||||
return config;
|
||||
@@ -62,6 +115,10 @@ public final class OpenChatPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region data
|
||||
|
||||
private OpenChatData data;
|
||||
public OpenChatData getData() {
|
||||
return data;
|
||||
@@ -90,37 +147,33 @@ public final class OpenChatPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
instance = this;
|
||||
loadConfig();
|
||||
loadData();
|
||||
getLogger().info("[OpenChat] Loaded.");
|
||||
// endregion
|
||||
|
||||
// region SensitiveWordFilter
|
||||
|
||||
private final SensitiveWordFilter sensitiveWordFilter = new SensitiveWordFilter();
|
||||
|
||||
public SensitiveWordFilter getSensitiveWordFilter() {
|
||||
return sensitiveWordFilter;
|
||||
}
|
||||
private static final String SENSITIVE_WORD_LIST_FILE_NAME = "SensitiveWordList.txt";
|
||||
public void loadSensitiveWordList() {
|
||||
try {
|
||||
var sensitiveWordListFile = new File(getDataFolder(), SENSITIVE_WORD_LIST_FILE_NAME);
|
||||
if (!sensitiveWordListFile.exists()) {
|
||||
var in = OpenChatPlugin.class.getClassLoader().getResourceAsStream(SENSITIVE_WORD_LIST_FILE_NAME);
|
||||
Files.copy(Objects.requireNonNull(in), sensitiveWordListFile.toPath());
|
||||
in.close();
|
||||
}
|
||||
var wordList = Files.readAllLines(sensitiveWordListFile.toPath());
|
||||
for (var word : wordList) {
|
||||
sensitiveWordFilter.addWord(word);
|
||||
}
|
||||
getLogger().info("[OpenChat] {} sensitive words loaded", wordList.size());
|
||||
} catch (Exception ex) {
|
||||
getLogger().error("[OpenChat] Failed to load sensitive word list!", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
// Register event listeners.
|
||||
// new EventHandler<>(PlayerJoinEvent.class)
|
||||
// .priority(HandlerPriority.NORMAL)
|
||||
// .listener(EventListeners::onJoin)
|
||||
// .register(this);
|
||||
|
||||
// Register commands.
|
||||
getHandle().registerCommand(new ChatServerCommands());
|
||||
getHandle().registerCommand(new ChatPlayerCommands());
|
||||
|
||||
// Set my chat system.
|
||||
getServer().setChatSystem(new OpenChatSystem(this));
|
||||
|
||||
// Log a plugin status message.
|
||||
getLogger().info("[OpenChat] Enabled, see https://github.com/jie65535/gc-openchat-plugin");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
saveData();
|
||||
saveConfig();
|
||||
getLogger().info("[OpenChat] Disabled.");
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
@@ -17,64 +17,170 @@
|
||||
*/
|
||||
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();
|
||||
// 构造MiniOneBot
|
||||
miniOneBot = new MiniOneBot(javalin, loadToken(), logger);
|
||||
// 启动WebSocket服务
|
||||
miniOneBot.startWsServer(plugin.getConfig().wsPath);
|
||||
// 订阅群消息事件
|
||||
miniOneBot.subscribeGroupMessageEvent(this::onGroupMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从配置文件中载入WsToken,如果为空则生成一个
|
||||
* @return Token
|
||||
*/
|
||||
private String loadToken() {
|
||||
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);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新载入Token
|
||||
*/
|
||||
public void reloadToken() {
|
||||
miniOneBot.setToken(loadToken());
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家进入服务器时触发
|
||||
*
|
||||
* @param event 事件
|
||||
*/
|
||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||
var player = event.getPlayer();
|
||||
// 发送上线消息到群里
|
||||
if (plugin.getConfig().sendLoginMessageToBot && plugin.getConfig().groupId >= 1) {
|
||||
miniOneBot.sendGroupMessage(plugin.getConfig().groupId, plugin.getConfig().loginMessageFormat
|
||||
.replace("{nickName}", player.getNickname())
|
||||
.replace("{uid}", String.valueOf(player.getUid())));
|
||||
}
|
||||
|
||||
// 发送上线消息到游戏
|
||||
if (plugin.getConfig().sendLoginMessageToGame) {
|
||||
broadcastChatMessage(plugin.getConfig().loginMessageFormatInGame
|
||||
.replace("{nickName}", player.getNickname())
|
||||
.replace("{uid}", String.valueOf(player.getUid())),
|
||||
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) {
|
||||
miniOneBot.sendGroupMessage(plugin.getConfig().groupId, plugin.getConfig().logoutMessageFormat
|
||||
.replace("{nickName}", player.getNickname())
|
||||
.replace("{uid}", String.valueOf(player.getUid())));
|
||||
}
|
||||
|
||||
// 发送离线消息到游戏
|
||||
if (plugin.getConfig().sendLogoutMessageToGame) {
|
||||
broadcastChatMessage(plugin.getConfig().logoutMessageFormatInGame
|
||||
.replace("{nickName}", player.getNickname())
|
||||
.replace("{uid}", String.valueOf(player.getUid())),
|
||||
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 +191,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 +202,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 +214,104 @@ 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);
|
||||
|
||||
// 转发给其它玩家
|
||||
broadcastChatMessage(formattedMessage, player.getUid());
|
||||
|
||||
// 转发到机器人
|
||||
if (plugin.getConfig().isSendToBot && plugin.getConfig().groupId >= 1) {
|
||||
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().groupId < 1 || event.groupId() != plugin.getConfig().groupId) return;
|
||||
|
||||
// 判断是否管理员发送的消息
|
||||
if (plugin.getConfig().adminIds.contains(event.senderId())
|
||||
&& !plugin.getConfig().adminPrefix.isEmpty() // 判断是否符合命令前缀
|
||||
&& event.message().startsWith(plugin.getConfig().adminPrefix)
|
||||
) {
|
||||
try {
|
||||
logger.info("Command used by op [{}({})]: {}", event.senderCardOrNickname(), event.senderId(), event.message());
|
||||
String rawCommand = event.message().substring(plugin.getConfig().adminPrefix.length());
|
||||
String result = EventListeners.runConsoleCommand(rawCommand);
|
||||
miniOneBot.sendGroupMessage(event.groupId(), result);
|
||||
} catch (Exception ex) {
|
||||
logger.error("Administrator command execution failed", ex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查开关
|
||||
if (!plugin.getConfig().isSendToGame) 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 纯文本消息
|
||||
*/
|
||||
private void broadcastChatMessage(String message) {
|
||||
broadcastChatMessage(message, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播聊天消息给除了指定玩家外所有玩家(未开启聊天玩家除外)
|
||||
*
|
||||
* @param message 纯文本消息
|
||||
* @param exceptId 排除在外的Uid
|
||||
*/
|
||||
private void broadcastChatMessage(String message, int exceptId) {
|
||||
for (Player p : getServer().getPlayers().values()) {
|
||||
// 将消息发送给除了自己以外所有未关闭聊天的玩家
|
||||
if (p != player && !plugin.getData().offChatPlayers.contains(p.getUid())) {
|
||||
if (p.getUid() != exceptId && !plugin.getData().offChatPlayers.contains(p.getUid())) {
|
||||
p.dropMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查玩家是否正在禁言中
|
||||
*
|
||||
* @param player 玩家对象
|
||||
* @return 是否禁言中
|
||||
*/
|
||||
private boolean checkIsBanning(Player player) {
|
||||
var banList = plugin.getData().banList;
|
||||
// 检测是否正在禁言中
|
||||
@@ -142,18 +327,24 @@ public class OpenChatSystem extends ChatSystem {
|
||||
|
||||
/**
|
||||
* 消息内容审查
|
||||
*
|
||||
* @param message 消息
|
||||
* @return 是否合法合规
|
||||
*/
|
||||
private boolean checkMessageModeration(String message) {
|
||||
// TODO see https://github.com/houbb/sensitive-word
|
||||
return !message.isEmpty();
|
||||
return !plugin.getSensitiveWordFilter().isSensitive(message);
|
||||
}
|
||||
|
||||
// region 发言频率限制
|
||||
|
||||
/**
|
||||
* 发言频率计时器
|
||||
*/
|
||||
Int2ObjectMap<LongArrayFIFOQueue> speakingTimes = new Int2ObjectOpenHashMap<>();
|
||||
|
||||
/**
|
||||
* 消息频率检查
|
||||
*
|
||||
* @param player 玩家对象
|
||||
* @return 是否在约定阈值内
|
||||
*/
|
||||
@@ -166,4 +357,6 @@ public class OpenChatSystem extends ChatSystem {
|
||||
list.dequeueLong();
|
||||
return list.size() <= plugin.getConfig().messageFreLimitPerMinute;
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
||||
@@ -26,7 +26,15 @@ 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>",
|
||||
"op|deop <userId(QQ)>",
|
||||
},
|
||||
permission = "server.chat",
|
||||
permissionTargeted = "server.chat.others",
|
||||
targetRequirement = Command.TargetRequirement.NONE)
|
||||
@@ -67,16 +75,19 @@ public class ChatServerCommands implements CommandHandler {
|
||||
CommandHandler.sendTranslatedMessage(sender, "commands.execution.need_target");
|
||||
return;
|
||||
}
|
||||
var time = 2051190000L;
|
||||
var timeMs = 0L;
|
||||
if (args.size() == 2) {
|
||||
try {
|
||||
time = System.currentTimeMillis() + Integer.parseInt(args.get(1)) * 60_000L;
|
||||
timeMs = System.currentTimeMillis() + Integer.parseInt(args.get(1)) * 60_000L;
|
||||
} catch (NumberFormatException ignored) {
|
||||
CommandHandler.sendTranslatedMessage(sender, "commands.ban.invalid_time");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// default ban 1 year
|
||||
timeMs = System.currentTimeMillis() + 1000L * 60L * 60L * 24L * 365L;
|
||||
}
|
||||
plugin.getData().banList.put(targetPlayer.getUid(), time);
|
||||
plugin.getData().banList.put(targetPlayer.getUid(), timeMs);
|
||||
plugin.saveData();
|
||||
CommandHandler.sendMessage(sender, "OK");
|
||||
}
|
||||
@@ -95,7 +106,40 @@ public class ChatServerCommands implements CommandHandler {
|
||||
CommandHandler.sendMessage(sender, "OK");
|
||||
}
|
||||
case "reload" -> {
|
||||
// 重载配置
|
||||
plugin.loadConfig();
|
||||
// 重载敏感词
|
||||
plugin.loadSensitiveWordList();
|
||||
// 重载令牌
|
||||
plugin.getOpenChatSystem().reloadToken();
|
||||
CommandHandler.sendMessage(sender, "OK");
|
||||
}
|
||||
case "group" -> {
|
||||
var groupId = 0L;
|
||||
try {
|
||||
groupId = Long.parseLong(args.get(1));
|
||||
} catch (Exception ignored) {
|
||||
sendUsageMessage(sender);
|
||||
return;
|
||||
}
|
||||
plugin.getConfig().groupId = groupId;
|
||||
plugin.saveConfig();
|
||||
CommandHandler.sendMessage(sender, "OK");
|
||||
}
|
||||
case "op", "deop" -> {
|
||||
var adminId = 0L;
|
||||
try {
|
||||
adminId = Long.parseLong(args.get(1));
|
||||
} catch (Exception ignored) {
|
||||
sendUsageMessage(sender);
|
||||
return;
|
||||
}
|
||||
if (subCommand.equals("op")) {
|
||||
plugin.getConfig().adminIds.add(adminId);
|
||||
} else {
|
||||
plugin.getConfig().adminIds.remove(adminId);
|
||||
}
|
||||
plugin.saveConfig();
|
||||
CommandHandler.sendMessage(sender, "OK");
|
||||
}
|
||||
default -> sendUsageMessage(sender);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* gc-openchat
|
||||
* Copyright (C) 2022 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.openchat.utils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class SensitiveWordFilter {
|
||||
|
||||
// 定义一个内部类表示图中的节点
|
||||
private static class Node {
|
||||
// 用一个哈希表存储该节点的后继节点
|
||||
private final Map<Character, Node> children = new HashMap<>();
|
||||
// 用一个布尔值表示该节点是否是某个敏感词的结尾
|
||||
private boolean isEnd = false;
|
||||
}
|
||||
|
||||
// 定义一个根节点
|
||||
private final Node root = new Node();
|
||||
|
||||
// 定义一个添加敏感词的方法
|
||||
public void addWord(String word) {
|
||||
if (word == null || word.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// 从根节点开始遍历
|
||||
Node current = root;
|
||||
for (char c : word.toCharArray()) {
|
||||
// 如果当前节点没有以c为键的后继节点,就创建一个新的节点并添加到哈希表中
|
||||
if (!current.children.containsKey(c)) {
|
||||
current.children.put(c, new Node());
|
||||
}
|
||||
// 更新当前节点为后继节点
|
||||
current = current.children.get(c);
|
||||
}
|
||||
// 标记当前节点为某个敏感词的结尾
|
||||
current.isEnd = true;
|
||||
}
|
||||
|
||||
// 定义一个检测聊天消息是否包含敏感词的方法
|
||||
public boolean isSensitive(String message) {
|
||||
if (message == null || message.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// 遍历聊天消息中的每个字符作为起始位置
|
||||
for (int i = 0; i < message.length(); i++) {
|
||||
// 从根节点开始遍历
|
||||
Node current = root;
|
||||
for (int j = i; j < message.length(); j++) {
|
||||
char c = message.charAt(j);
|
||||
// 如果当前节点没有以c为键的后继节点,说明没有匹配到敏感词,跳出循环
|
||||
if (!current.children.containsKey(c)) {
|
||||
break;
|
||||
}
|
||||
// 更新当前节点为后继节点
|
||||
current = current.children.get(c);
|
||||
// 如果当前节点是某个敏感词的结尾,说明匹配到了敏感词,返回true
|
||||
if (current.isEnd) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 遍历完聊天消息没有匹配到任何敏感词,返回false
|
||||
return false;
|
||||
}
|
||||
}
|
||||
576
src/main/resources/SensitiveWordList.txt
Normal file
576
src/main/resources/SensitiveWordList.txt
Normal file
@@ -0,0 +1,576 @@
|
||||
18禁
|
||||
a片
|
||||
caonima
|
||||
cnm
|
||||
caotama
|
||||
cao你
|
||||
cao你妈
|
||||
fa轮
|
||||
fuck
|
||||
hjt
|
||||
jb
|
||||
jzm
|
||||
nmsl
|
||||
sb
|
||||
vpn
|
||||
zhengfu
|
||||
zheng府
|
||||
zf
|
||||
zedong
|
||||
亂倫
|
||||
噴精
|
||||
姦淫
|
||||
屄
|
||||
屄毛
|
||||
幹炮
|
||||
幹砲
|
||||
擠乳汁
|
||||
溫家寶
|
||||
無毛穴
|
||||
獸交
|
||||
爱女人
|
||||
爱液
|
||||
扒穴
|
||||
拔屄自拍
|
||||
白虎阴穴
|
||||
白虎少妇
|
||||
白浆四溅
|
||||
包二奶
|
||||
薄熙来
|
||||
薄码
|
||||
暴奸
|
||||
爆乳娘
|
||||
爆草
|
||||
爆操
|
||||
被操
|
||||
被插
|
||||
被干
|
||||
逼痒
|
||||
逼奸
|
||||
博彩
|
||||
擦你妈
|
||||
操我
|
||||
操死
|
||||
操死你
|
||||
操你妈
|
||||
操你奶
|
||||
操你姐
|
||||
操逼
|
||||
操你祖宗
|
||||
操你大爷
|
||||
操你妹
|
||||
操穴
|
||||
操屄
|
||||
操他妈
|
||||
操你全家
|
||||
操烂
|
||||
操妻
|
||||
操你嘴
|
||||
操死她
|
||||
操b
|
||||
操它妈
|
||||
曹刚川
|
||||
草你祖宗
|
||||
草你大爷
|
||||
草你妈
|
||||
草你吗
|
||||
草bi
|
||||
草她妈
|
||||
草拟吗
|
||||
草你娘
|
||||
草他妈
|
||||
厕所盗摄
|
||||
厕奴
|
||||
插阴茎
|
||||
插逼
|
||||
插妹妹
|
||||
插穴止痒
|
||||
插死她
|
||||
插比
|
||||
插b
|
||||
插你
|
||||
陈同海
|
||||
陈水扁
|
||||
陈良宇
|
||||
成人片
|
||||
成人网站
|
||||
成人图
|
||||
成人电
|
||||
成人文
|
||||
成人视
|
||||
成人自拍
|
||||
成人小
|
||||
惩公安
|
||||
吃鸡巴
|
||||
抽插
|
||||
臭鸡八
|
||||
臭鸡吧
|
||||
床上写真
|
||||
催情藥
|
||||
催情药
|
||||
催眠水
|
||||
催情粉
|
||||
大奶子
|
||||
大肉棒
|
||||
大奶头
|
||||
大明运气咒
|
||||
大力抽送
|
||||
大血逼
|
||||
大雞巴
|
||||
大鸡巴
|
||||
大傻b
|
||||
戴秉国
|
||||
盗撮
|
||||
邓小瓶
|
||||
邓小平
|
||||
邓爷爷
|
||||
东北独立
|
||||
杜世成
|
||||
杜德印
|
||||
法lun
|
||||
法伦功
|
||||
法维权
|
||||
法一轮
|
||||
法正乾
|
||||
法车仑
|
||||
法轮佛
|
||||
法轮
|
||||
翻墙
|
||||
反共复清
|
||||
反华示威
|
||||
肥逼
|
||||
粉穴
|
||||
风艳阁
|
||||
干你妈
|
||||
干的爽
|
||||
干穴
|
||||
干你娘
|
||||
干你妹
|
||||
干死你
|
||||
干你全家
|
||||
肛交
|
||||
肛门
|
||||
肛门拳交
|
||||
给你爽
|
||||
根达亚文明
|
||||
共残主义
|
||||
狗娘养
|
||||
灌满精液
|
||||
郭金龙
|
||||
国峰
|
||||
国锋
|
||||
含屌
|
||||
喝血社会
|
||||
黑毛屄
|
||||
胡云松
|
||||
胡王八
|
||||
胡谨涛
|
||||
胡錦濤
|
||||
胡海峰
|
||||
胡主席
|
||||
胡春华
|
||||
胡温
|
||||
虎精逃
|
||||
华国
|
||||
黄色电影
|
||||
回良玉
|
||||
混蛋
|
||||
激情电
|
||||
激情妹
|
||||
激情炮
|
||||
激情短
|
||||
激情小说
|
||||
鸡巴
|
||||
鸡奸
|
||||
集体自杀
|
||||
挤乳汁
|
||||
几吧
|
||||
妓女
|
||||
家宝
|
||||
奸幼
|
||||
奸杀
|
||||
奸污
|
||||
践货
|
||||
贱人
|
||||
贱比
|
||||
江泽民
|
||||
江澤民
|
||||
江x
|
||||
江某某
|
||||
脚奴
|
||||
街头扒衣
|
||||
金毛穴
|
||||
锦涛
|
||||
精子射在
|
||||
警察说保
|
||||
警方包庇
|
||||
警车雷达
|
||||
警察殴打
|
||||
警察的幌
|
||||
就去日
|
||||
菊花洞
|
||||
巨乳
|
||||
恐怖份子
|
||||
恐怖分子
|
||||
抠穴
|
||||
口淫
|
||||
口交
|
||||
口活
|
||||
口内爆射
|
||||
狂乳激揺
|
||||
拉登
|
||||
浪逼
|
||||
浪叫
|
||||
雷管
|
||||
李小鹏
|
||||
李鹏
|
||||
李克强
|
||||
李洪志
|
||||
李世民
|
||||
習近平
|
||||
莲花逼
|
||||
炼大法
|
||||
梁光烈
|
||||
两会又三
|
||||
两会代
|
||||
刘延东
|
||||
露逼
|
||||
露b
|
||||
乱伦类
|
||||
乱奸
|
||||
乱伦小
|
||||
轮子功
|
||||
轮奸
|
||||
轮功
|
||||
伦理毛
|
||||
伦理电影
|
||||
伦理片
|
||||
伦理大
|
||||
裸舞视
|
||||
裸体
|
||||
裸聊网
|
||||
妈个逼
|
||||
妈了个逼
|
||||
妈了逼
|
||||
麻果配
|
||||
麻果丸
|
||||
麻古
|
||||
玛雅历法
|
||||
卖淫
|
||||
满狗
|
||||
肏屄
|
||||
氓培训
|
||||
猫贼洞
|
||||
毛主席
|
||||
毛遮洞
|
||||
毛则东
|
||||
毛泽东
|
||||
毛贼东
|
||||
毛澤東
|
||||
美女高潮
|
||||
美艳少妇
|
||||
妹按摩
|
||||
妹上门
|
||||
门按摩
|
||||
门保健
|
||||
蒙汗药
|
||||
孟建柱
|
||||
迷昏口
|
||||
迷奸
|
||||
迷魂香
|
||||
迷幻型
|
||||
迷幻药
|
||||
迷情药
|
||||
迷幻藥
|
||||
迷魂药
|
||||
迷藥
|
||||
迷奸药
|
||||
迷昏药
|
||||
迷昏藥
|
||||
迷魂藥
|
||||
迷情水
|
||||
谜奸药
|
||||
秘唇
|
||||
蜜穴
|
||||
密穴
|
||||
民抗议
|
||||
明慧网
|
||||
摸阴蒂
|
||||
某锦涛
|
||||
母奸
|
||||
母子乱伦
|
||||
母子奸情
|
||||
奶子
|
||||
男奴
|
||||
男女交欢
|
||||
内射
|
||||
嫩穴
|
||||
嫩b
|
||||
嫩逼
|
||||
嫩屄
|
||||
嫩bb
|
||||
嫩阴
|
||||
你妈死了
|
||||
你日妈
|
||||
你妈逼
|
||||
娘西皮
|
||||
娘了个比
|
||||
娘两腿之间
|
||||
浓精
|
||||
怒的志愿
|
||||
女优
|
||||
女任职名
|
||||
女人和狗
|
||||
女技师
|
||||
女激情
|
||||
女優
|
||||
女上门
|
||||
女被人家搞
|
||||
拍肩神药
|
||||
炮友
|
||||
喷尿
|
||||
屁眼
|
||||
平叫到床
|
||||
平惨案
|
||||
仆不怕饮
|
||||
普通嘌
|
||||
期货配
|
||||
奇迹的黄
|
||||
奇淫散
|
||||
强暴
|
||||
强硬发言
|
||||
强奸你妹
|
||||
强奸
|
||||
强权政府
|
||||
巧淫奸戏
|
||||
情色
|
||||
全裸
|
||||
全家死绝
|
||||
全家死光
|
||||
群奸暴
|
||||
群体性事
|
||||
群起抗暴
|
||||
群交
|
||||
绕过封锁
|
||||
人类灭亡
|
||||
人兽
|
||||
人妻
|
||||
人妻做爱
|
||||
人妻熟女
|
||||
人妻榨乳
|
||||
人体炸弹
|
||||
人妻色诱
|
||||
日中断交
|
||||
日你妹
|
||||
日烂
|
||||
日你全家
|
||||
日死你
|
||||
日你妈
|
||||
日逼
|
||||
肉棒
|
||||
肉壶
|
||||
肉蒲团
|
||||
肉便器
|
||||
肉茎
|
||||
肉淫器吞精
|
||||
肉棍
|
||||
肉穴
|
||||
肉逼
|
||||
肉棍干骚妇
|
||||
肉唇
|
||||
肉洞
|
||||
乳交
|
||||
软弱的国
|
||||
三秒倒
|
||||
三级片
|
||||
三挫
|
||||
三唑
|
||||
搔逼
|
||||
骚妇
|
||||
骚浪美女
|
||||
骚贱
|
||||
骚嘴
|
||||
骚姐姐
|
||||
骚母
|
||||
骚洞
|
||||
骚穴
|
||||
骚逼
|
||||
骚妹
|
||||
骚浪
|
||||
骚乳
|
||||
骚女
|
||||
骚水
|
||||
色妹妹
|
||||
色电影
|
||||
色视频
|
||||
色猫
|
||||
色小说
|
||||
色书库
|
||||
傻臂
|
||||
傻逼
|
||||
傻b
|
||||
傻避
|
||||
煞笔
|
||||
煞逼
|
||||
少妇
|
||||
舌头穴
|
||||
射颜
|
||||
射爽
|
||||
社会主义灭亡
|
||||
沈跃跃
|
||||
十八禁
|
||||
兽奸
|
||||
熟妇人妻
|
||||
爽片
|
||||
爽穴
|
||||
死全家
|
||||
死逼
|
||||
苏树林
|
||||
酥穴
|
||||
塔利班
|
||||
蘚鮑
|
||||
台湾
|
||||
台独
|
||||
台湾独立
|
||||
体奸
|
||||
天黯门
|
||||
天岸门
|
||||
天案们
|
||||
天安门大屠杀
|
||||
天氨门
|
||||
天安门
|
||||
天胺门
|
||||
天案门
|
||||
舔脚
|
||||
舔屄
|
||||
铁凝
|
||||
偷窥图片
|
||||
推背图
|
||||
退党
|
||||
吞精
|
||||
王胜俊
|
||||
王岐山
|
||||
王洛林
|
||||
王太华
|
||||
王鸿举
|
||||
瘟总理
|
||||
瘟加饱
|
||||
瘟假饱
|
||||
温影帝
|
||||
温家堡
|
||||
温切斯特
|
||||
温家宝
|
||||
温家某
|
||||
吴定富
|
||||
吴邦国
|
||||
吸精少女
|
||||
习近平
|
||||
洗肠射尿
|
||||
下流地带
|
||||
下贱
|
||||
销魂洞
|
||||
小穴
|
||||
小平遗言
|
||||
小逼崽子
|
||||
小平遗嘱
|
||||
新唐人
|
||||
性虐
|
||||
性奴
|
||||
性爱
|
||||
性爱图库
|
||||
颜射
|
||||
艳妇淫女
|
||||
要射精了
|
||||
要泄了
|
||||
要射了
|
||||
要人权
|
||||
耀邦
|
||||
叶剑英
|
||||
夜激情
|
||||
液体炸
|
||||
一夜欢
|
||||
遗嘱小平
|
||||
阴屄
|
||||
阴b
|
||||
阴茎
|
||||
阴唇
|
||||
阴部
|
||||
阴締
|
||||
淫贱
|
||||
淫叫
|
||||
淫液
|
||||
淫娃
|
||||
淫魔舞
|
||||
淫情
|
||||
淫肉
|
||||
淫蜜
|
||||
淫蕩
|
||||
淫汁
|
||||
淫姐
|
||||
淫妞
|
||||
淫逼
|
||||
淫亂潮吹
|
||||
淫荡
|
||||
淫騷妹
|
||||
淫情女
|
||||
淫奴
|
||||
淫兽
|
||||
淫兽学
|
||||
淫水
|
||||
淫妇
|
||||
淫穴
|
||||
淫水爱液
|
||||
淫母
|
||||
淫亂
|
||||
幼交
|
||||
玉穴
|
||||
原味内裤
|
||||
原味内衣
|
||||
援交妹
|
||||
杂种操的
|
||||
则民爷爷
|
||||
泽民
|
||||
炸立交
|
||||
炸弹遥控
|
||||
炸药的制
|
||||
炸药
|
||||
炸弹
|
||||
炸药配
|
||||
炸弹教
|
||||
炸鸟巢
|
||||
炸广州
|
||||
炸药制
|
||||
炸学校
|
||||
张志国
|
||||
张德江
|
||||
张荣坤
|
||||
政治局常委
|
||||
政fu
|
||||
政府
|
||||
制服狩
|
||||
中国人权
|
||||
中日断交
|
||||
朱镕基
|
||||
猪容鸡
|
||||
诸世纪
|
||||
主席
|
||||
主席像
|
||||
自焚
|
||||
自拍美穴
|
||||
自慰抠穴
|
||||
自慰
|
||||
总竖鸡
|
||||
总理
|
||||
总书记
|
||||
钻插
|
||||
做爱
|
||||
妞上门
|
||||
嫖妓指南
|
||||
嫖鸡
|
||||
嫖俄罗
|
||||
門服務
|
||||
陰道
|
||||
陰唇
|
||||
陰戶
|
||||
掰穴
|
||||
騷浪
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "openchat-plugin",
|
||||
"description": "Chat with players in the server console",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"mainClass": "com.github.jie65535.openchat.OpenChatPlugin",
|
||||
"authors": ["jie65535"]
|
||||
"authors": ["jie65535"],
|
||||
"api": 2
|
||||
}
|
||||
Reference in New Issue
Block a user