16 Commits

Author SHA1 Message Date
01a2feec03 Update API Version to 2 2023-09-03 10:09:33 +08:00
04b8d2dc77 update plugin api version to 3 2023-09-02 18:03:04 +08:00
4129eaa13e update plugin version to v1.0.0 2023-09-02 18:02:51 +08:00
dc90a17824 feat: implement group command response 2023-09-02 18:02:20 +08:00
e795c5a9c4 Upgrade to 1.6.0 2023-06-03 00:12:41 +08:00
28456bb179 Update version to 0.2.1 2023-04-09 20:42:30 +08:00
397cf392f5 Implement token reloading (fix #2) 2023-04-09 20:41:18 +08:00
d9ef6b3097 Update en translation 2023-03-13 21:40:58 +08:00
5923f2c0eb Add OpenChat connection guide 2023-03-13 21:40:23 +08:00
443f04f910 Update TODO List 2023-03-05 18:58:33 +08:00
36ede5e634 Update onebot chat sample 2023-03-05 18:54:07 +08:00
61b9be9888 Implement sensitive word filter 2023-03-05 18:50:24 +08:00
2a696b4854 Implement op execution command
Add in-game online and offline broadcast
2023-03-05 17:10:03 +08:00
b119bb0cb9 Fix incorrect default ban duration 2023-03-05 16:53:49 +08:00
d8caa42899 Implement OneBot protocol
Add subcommand `group`
Update version to v0.2.0
Update plugin config
2023-03-05 10:27:14 +08:00
360NENZ
d060b1804f Add translation (#1)
* Added 繁中 and RU redeirect on all ReadMe

* Added zh-TW translation

* Added ru-RU translation
2023-01-13 22:51:31 +08:00
23 changed files with 2387 additions and 100 deletions

3
.idea/misc.xml generated
View File

@@ -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>

View File

@@ -6,7 +6,7 @@
[![GitHub release](https://img.shields.io/github/v/release/jie65535/gc-openchat-plugin)](https://github.com/jie65535/gc-openchat-plugin/releases/latest)
[![Build](https://github.com/jie65535/gc-openchat-plugin/actions/workflows/build.yml/badge.svg)](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
![Bot Connect example](/doc/Chat-OneBot.png)
### 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
View File

@@ -0,0 +1,75 @@
# gc-openchat-plugin
[![GitHub license](https://img.shields.io/github/license/jie65535/gc-openchat-plugin)](https://github.com/jie65535/gc-openchat-plugin/blob/main/LICENSE)
[![GitHub stars](https://img.shields.io/github/stars/jie65535/gc-openchat-plugin)](https://github.com/jie65535/gc-openchat-plugin/stargazers)
[![Github All Releases](https://img.shields.io/github/downloads/jie65535/gc-openchat-plugin/total.svg)](https://github.com/jie65535/gc-openchat-plugin/releases)
[![GitHub release](https://img.shields.io/github/v/release/jie65535/gc-openchat-plugin)](https://github.com/jie65535/gc-openchat-plugin/releases/latest)
[![Build](https://github.com/jie65535/gc-openchat-plugin/actions/workflows/build.yml/badge.svg)](https://github.com/jie65535/gc-openchat-plugin/actions/workflows/build.yml)
[简中](README.md) | [繁中](README-zh-TW.md) | [EN](README-en-US.md) | RU
Разрешить игрокам общаться внутри сервера
![Пример чата](doc/Chat.png)
Разговор с учетной записью сервера в игре эквивалентен отправке на мировой канал, и все игроки на сервере могут получить сообщение.
## 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
View File

@@ -0,0 +1,75 @@
# gc-openchat-plugin
[![GitHub license](https://img.shields.io/github/license/jie65535/gc-openchat-plugin)](https://github.com/jie65535/gc-openchat-plugin/blob/main/LICENSE)
[![GitHub stars](https://img.shields.io/github/stars/jie65535/gc-openchat-plugin)](https://github.com/jie65535/gc-openchat-plugin/stargazers)
[![Github All Releases](https://img.shields.io/github/downloads/jie65535/gc-openchat-plugin/total.svg)](https://github.com/jie65535/gc-openchat-plugin/releases)
[![GitHub release](https://img.shields.io/github/v/release/jie65535/gc-openchat-plugin)](https://github.com/jie65535/gc-openchat-plugin/releases/latest)
[![Build](https://github.com/jie65535/gc-openchat-plugin/actions/workflows/build.yml/badge.svg)](https://github.com/jie65535/gc-openchat-plugin/actions/workflows/build.yml)
[简中](README.md) | 繁中 | [EN](README-en-US.md) | [RU](README-ru-RU.md)
讓玩家在服務器內聊天
![聊天示例](doc/Chat.png)
在遊戲內與服務器賬號對話,相當於發送到世界頻道,服務器內所有玩家均可收到消息。
## 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
View File

@@ -6,7 +6,7 @@
[![GitHub release](https://img.shields.io/github/v/release/jie65535/gc-openchat-plugin)](https://github.com/jie65535/gc-openchat-plugin/releases/latest)
[![Build](https://github.com/jie65535/gc-openchat-plugin/actions/workflows/build.yml/badge.svg)](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)
![群服互联聊天示例](/doc/Chat-OneBot.png)
除了登录设置外,需[配置](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)

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

View 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("&", "&amp;")
// .replace("[", "&#91;")
// .replace("]", "&#93;")
// .replace(",", "&#44;");
// }
private static String unescape(String msg) {
return msg.replace("&amp;", "&")
.replace("&#91;", "[")
.replace("&#93;", "]")
.replace("&#44;", ",");
}
// endregion
}

View File

@@ -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);
// }
// }
//}

View File

@@ -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();
}
}

View 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);
}
}

View File

@@ -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
) {
}

View File

@@ -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);
}

View File

@@ -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
) {
}

View File

@@ -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);
}

View File

@@ -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());
}
}

View File

@@ -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>";
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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);

View File

@@ -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;
}
}

View 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
政府
制服狩
中国人权
中日断交
朱镕基
猪容鸡
诸世纪
主席
主席像
自焚
自拍美穴
自慰抠穴
自慰
总竖鸡
总理
总书记
钻插
做爱
妞上门
嫖妓指南
嫖鸡
嫖俄罗
門服務
陰道
陰唇
陰戶
掰穴
騷浪

View File

@@ -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
}