11 Commits

Author SHA1 Message Date
781979f937 Update to allow batch execution of commands (#4) 2022-09-17 12:46:23 +08:00
93f3235dd0 Update to allow alias commands as public commands 2022-09-17 09:16:36 +08:00
33134ad01f Update to group message sync only 2022-09-17 09:07:54 +08:00
25498086a2 Update version to v0.3.0
Add user message support
Add default server setting
2022-09-16 23:38:08 +08:00
78a3c09b43 Fix default config issue (remove / prefix)
Fix public command not handling aliases
2022-09-13 21:16:15 +08:00
137bf7d4f5 Update README 2022-09-12 23:33:30 +08:00
962b439d47 Rename publicCommand to publicCommands 2022-09-12 23:31:17 +08:00
caaf10b813 Update version to v0.2.1 2022-09-12 23:29:42 +08:00
7e50624261 Add Public Commands 2022-09-12 23:29:34 +08:00
08231d4c27 Update version to v0.2.0 2022-09-11 23:58:24 +08:00
4662731543 Fix Https cert issue(#1) 2022-09-11 23:58:10 +08:00
8 changed files with 257 additions and 76 deletions

View File

@@ -1,18 +1,24 @@
# J Grasscutter Command
# 在QQ群里远程执行命令的插件
This repo is only used for the Chinese social software QQ, so only the Chinese version is available.
# 用QQ执行GC命令的机器人插件
- 基于 [Mirai-Console](https://github.com/mamoe/mirai-console) 开发的插件
- 服务端必须使用 [OpenCommand](https://github.com/jie65535/gc-opencommand-plugin) 插件
> Mirai机器人相关文档请参阅 [用户手册](https://github.com/mamoe/mirai/blob/dev/docs/UserManual.md)
> 本项目**不会教你**如何安装和登录机器人请自行了解Mirai相关信息。_目前暂不考虑其它框架或平台有意者可自行移植_
Mirai机器人相关文档请参阅 [用户手册](https://github.com/mamoe/mirai/blob/dev/docs/UserManual.md)
本项目**不会教你**如何安装和登录机器人请自行了解Mirai相关信息。
- 服务端必须使用 [OpenCommand](https://github.com/jie65535/gc-opencommand-plugin) 插件
- 若使用后觉得满意,可以给我一个 Star 作为鼓励 ; )
- 若有问题或者建议,欢迎提出 Issue 进行反馈。
- 建议 Watch 本项目以接收更新推送。
# 插件用法
## 首先使用指令添加一个服务器
```shell
# 用法
/jgc addServer <address> [name] [description] # 添加服务器
/jgc addServer <address> [name] [description] # 添加服务器,首个服务器为默认服务器,可切换默认服务器
# 示例
/jgc addServer http://127.0.0.1:443 测试服 本地测试服务器
# 成功返回 "服务器已添加ID为[1]使用servers子命令查看服务器列表"
@@ -33,18 +39,22 @@ Mirai机器人相关文档请参阅 [用户手册](https://github.com/mamoe/mira
## 将服务器绑定到机器人所在群聊
```shell
# 用法
/jgc linkGroup/bindGroup/addGroup <serverId> [group] # 绑定服务器到群,在群内执行可忽略群参数
/jgc linkGroup/bindGroup/addGroup [serverId] [group] # 绑定服务器到群,若未指定服务器则使用默认服务器ID在群内执行可忽略群参数
# 示例(控制台)
/jgc linkGroup 1 群号
/jgc linkGroup 1 123456 # 指定将服务器[1]绑定到群[123456]
# 示例(群里)
/jgc linkGroup 1
/jgc linkGroup 1 # 指定绑定到服务器[1]
/jgc linkGroup # 忽略服务器参数,将使用默认服务器
/jgc enable # 若启用的群未绑定,将自动绑定到默认服务器
# 成功返回 "OK"
```
在聊天环境执行 Mirai-Console 命令需要另一个插件 [Chat Command](https://github.com/project-mirai/chat-command)
执行GC命令不需要这个见后文
## 绑定账号
玩家想要在群里执行命令需要绑定自己的游戏UID需要在群里发送 `绑定 <uid>` 来向目标账号发送验证码,然后将验证码发到群里完成验证,如图所示
玩家想要在群里执行命令需要绑定自己的游戏UID
需要在群里发送 `绑定 <uid>` 来向目标账号发送验证码,
然后将验证码发到群里完成验证,如图所示
![群内验证示例图](screenshot/verification.png)
@@ -79,6 +89,28 @@ _可以通过 `/jgc setBindCommand <prefix>` 来修改执行命令前缀 _
![At群员示例图](screenshot/runAt.png)
---
你还可以一次性执行多条命令,并且可以通过在别名中设置多行命令来实现组合命令
例如:
```shell
!give 102 9999
give 203 999
```
![多行命令示例图](screenshot/batch.jpg)
还可以设置别名为多条命令,用`|`分隔,例如:
`/jgc setCommand 新手礼包 give 102 9999give 202 99give 203 99`
然后通过别名批量执行命令,例如:`!新手礼包`
## 私聊执行
v0.3.0 开始,玩家可以**私聊机器人**进行账号的绑定和命令的执行,
但是目前只能在**默认服务器**中执行,无法指定执行的服务器。
## 拉黑用户
如果你想禁止某个用户使用本插件执行命令,可以使用 `/ban <qq>` 来拉黑,使用 `/unban <qq>` 可以解除黑名单
_只是一个凭想象增加的功能也许能用上呢_
@@ -113,16 +145,23 @@ bindCommand: 绑定
commandPrefix: !
# 命令别名
commandAlias:
无敌: '/prop god on'
关闭无敌: '/prop god off'
无限体力: '/prop ns on'
关闭无限体力: '/prop ns off'
无限能量: '/prop ue on'
关闭无限能量: '/prop ue off'
点亮地图: '/prop unlockmap 1'
解锁地图: '/prop unlockmap 1'
位置: '/pos'
坐标: '/pos'
无敌: 'prop god on'
关闭无敌: 'prop god off'
无限体力: 'prop ns on'
关闭无限体力: 'prop ns off'
无限能量: 'prop ue on'
关闭无限能量: 'prop ue off'
点亮地图: 'prop unlockmap 1'
解锁地图: 'prop unlockmap 1'
位置: 'pos'
坐标: 'pos'
# 公开命令,无需绑定账号也可以执行(可用别名)(必须绑定了控制台令牌才可使用)
publicCommands:
- 'list'
- 'list uid'
# 默认服务器ID未指定服务器ID的命令将使用默认服务器执行。
# 私聊默认使用该服务器。
defaultServerId: 1
```
# 指令列表
@@ -154,15 +193,18 @@ commandAlias:
## 群相关
```shell
/jgc linkGroup/bindGroup/addGroup <serverId> [group] # 绑定服务器到群,在群内执行可忽略群参数
/jgc enable [group] # 启用指定群执行,在群内执行可忽略群参数
/jgc linkGroup/bindGroup/addGroup [serverId] [group] # 绑定服务器到群,若未指定服务器则使用默认服务器ID在群内执行可忽略群参数
/jgc enable [group] # 启用指定群执行,若未绑定,则自动绑定到默认服务器,在群内执行可忽略群参数
/jgc disable [group] # 禁用指定群执行,在群内执行可忽略群参数
```
## 命令别名相关
## 命令相关
```shell
/jgc setCommand <alias> <command> # 添加命令别名
/jgc listCommands # 列出所有别名
/jgc setCommand <alias> <command> # 添加命令别名(聊天执行可传多行命令)
/jgc removeCommand <alias> # 删除命令别名
/jgc addPublicCommand <command> # 添加公开命令(可用别名)(游客可用)
/jgc removePublicCommand <command> # 删除公开命令
```
# 实体结构

View File

@@ -7,7 +7,7 @@ plugins {
}
group = "top.jie65535.mirai"
version = "0.1.0"
version = "0.3.0"
repositories {
maven("https://maven.aliyun.com/repository/public")

BIN
screenshot/batch.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

View File

@@ -40,7 +40,7 @@ object JGrasscutterCommand : KotlinPlugin(
JvmPluginDescription(
id = "top.jie65535.mirai.grasscutter-command",
name = "J Grasscutter Command",
version = "0.1.0",
version = "0.3.0",
) {
author("jie65535")
info("""聊天执行GC命令""")
@@ -53,23 +53,29 @@ object JGrasscutterCommand : KotlinPlugin(
val eventChannel = GlobalEventChannel.parentScope(this)
// 监听群消息
eventChannel.subscribeAlways<GroupMessageEvent> {
eventChannel.subscribeAlways<MessageEvent> {
// 忽略被拉黑的用户发送的消息
if (PluginConfig.blacklist.contains(sender.id))
return@subscribeAlways
// 忽略未启用的群消息
val groupConfig = PluginData.groups.find { it.id == group.id }
if (groupConfig == null || !groupConfig.isEnabled)
return@subscribeAlways
val server = if (this is GroupMessageEvent) {
// 若为群消息,忽略未启用的群
val groupConfig = PluginData.groups.find { it.id == group.id }
if (groupConfig == null || !groupConfig.isEnabled)
return@subscribeAlways
// 获取群绑定的服务器
PluginData.servers.find { it.id == groupConfig.serverId }
} else {
// 否则为私聊消息,使用默认服务器
PluginData.servers.find { it.id == PluginConfig.defaultServerId }
}
// 忽略未启用的服务器
val server = PluginData.servers.find { it.id == groupConfig.serverId }
if (server == null || !server.isEnabled)
return@subscribeAlways
// 解析消息
val message = this.message.joinToString("") {
var message = this.message.joinToString("") {
if (it is At) {
// 替换@群员为@其绑定的Uid
val user = PluginData.users.find { user -> user.id == it.target && user.serverId == server.id }
@@ -104,19 +110,17 @@ object JGrasscutterCommand : KotlinPlugin(
}
// 处理执行游戏命令
else if (message.startsWith(PluginConfig.commandPrefix)) {
var command = message.removePrefix(PluginConfig.commandPrefix).trim()
if (command.isEmpty()) {
message = message.removePrefix(PluginConfig.commandPrefix).trim()
if (message.isEmpty()) {
return@subscribeAlways
}
// 检查是否使用别名
val t = PluginConfig.commandAlias[command]
if (!t.isNullOrEmpty()) command = t
// 如果是斜杠开头,则移除斜杠,在控制台执行不需要斜杠
if (command[0] == '/') {
command = command.substring(1)
if (command.isEmpty())
return@subscribeAlways
}
var command = PluginConfig.commandAlias[message]
command = if (command.isNullOrEmpty())
message
else
command.replace('|', '\n') // 若为多行命令,替换为换行
// 执行的用户
var user: User? = null
@@ -130,20 +134,27 @@ object JGrasscutterCommand : KotlinPlugin(
// 普通用户
user = PluginData.users.find { it.id == sender.id && it.serverId == server.id }
if (user == null || user.token.isEmpty()) {
return@subscribeAlways
if (server.consoleToken.isNotEmpty() // 仅服务器绑定了控制台令牌时
&& (PluginConfig.publicCommands.contains(command) // 检测执行的命令是否为公开命令
|| PluginConfig.publicCommands.contains(message) // 检测执行命令的原文是否为公开命令
)) {
// 允许游客执行控制台命令
logger.info("游客用户 ${sender.nameCardOrNick}(${sender.id}) 执行公开命令:$command")
server.consoleToken
} else {
return@subscribeAlways
}
} else {
logger.info("用户 ${sender.nameCardOrNick}(${sender.id}) 执行命令:$command")
// 使用用户缓存令牌
user.token
}
logger.info("用户 ${sender.nameCardOrNick}(${sender.id}) 执行命令:$command")
// 使用用户缓存令牌
user.token
}
try {
// 调用接口执行命令
val response = OpenCommandApi.runCommand(server.address, token, command)
if (response.isNullOrEmpty()) {
subject.sendMessage(this.message.quote() + "OK")
} else {
subject.sendMessage(this.message.quote() + response)
}
val response = OpenCommandApi.runCommands(server.address, token, command)
subject.sendMessage(this.message.quote() + response)
if (user != null) {
// 计数并更新最后运行时间
++user.runCount
@@ -175,10 +186,10 @@ object JGrasscutterCommand : KotlinPlugin(
logger.warning("${sender.nameCardOrNick}(${sender.id}) 在执行命令时发生异常", e)
}
}
// 否则如果启用了同步消息,且控制台令牌不为空
else if (server.consoleToken.isNotEmpty() && server.syncMessage) {
// 否则如果启用了同步消息,且控制台令牌不为空,且为群消息时
else if (server.consoleToken.isNotEmpty() && server.syncMessage && this is GroupMessageEvent) {
try {
OpenCommandApi.runCommand(
OpenCommandApi.runCommands(
server.address,
server.consoleToken,
"say <color=green>${sender.nameCardOrNick}</color>:\n${this.message.contentToString()}")

View File

@@ -242,13 +242,26 @@ object PluginCommands : CompositeCommand(
}
}
@SubCommand
@Description("设置默认服务器(初始值为首个创建的服务器)")
suspend fun CommandSender.setDefaultServer(id: Int) {
val server = PluginData.servers.find { it.id == id }
if (server == null) {
sendMessage("未找到指定服务器")
} else {
PluginConfig.defaultServerId = id
logger.info("已将 [$id] ${server.name} 设置为默认服务器")
sendMessage("OK")
}
}
// endregion 服务器相关命令
// region 群相关命令
@SubCommand("linkGroup", "bindGroup", "addGroup")
@Description("绑定服务器到群")
suspend fun CommandSender.linkGroup(serverId: Int, group: Group? = getGroupOrNull()) {
@Description("绑定服务器到群若未指定服务器则使用默认服务器ID")
suspend fun CommandSender.linkGroup(serverId: Int = PluginConfig.defaultServerId, group: Group? = getGroupOrNull()) {
if (group == null) {
sendMessage("必须指定群")
return
@@ -256,7 +269,7 @@ object PluginCommands : CompositeCommand(
val server = PluginData.servers.find { it.id == serverId }
if (server == null) {
sendMessage("指定服务器ID不存在,请先添加服务器(使用addServer子命令)")
sendMessage("指定服务器不存在,请先添加服务器(使用addServer子命令)")
return
}
@@ -272,14 +285,23 @@ object PluginCommands : CompositeCommand(
}
@SubCommand
@Description("启用指定群执行")
@Description("启用指定群执行,若未绑定,则自动绑定到默认服务器")
suspend fun CommandSender.enable(group: Group? = getGroupOrNull()) {
if (group == null) {
sendMessage("必须指定群")
} else {
val g = PluginData.groups.find { it.id == group.id }
if (g == null) {
sendMessage("请先绑定群到服务器(使用linkGroup子命令)")
// 当启用的群是未初始化的群时,使用默认服务器进行初始化
val server = PluginData.servers.find { it.id == PluginConfig.defaultServerId }
if (server != null) {
// 绑定到默认服务器
logger.info("将默认服务器[${server.id}] ${server.name} ${server.address} 绑定到群 ${group.name}(${group.id})")
PluginData.groups.add(GroupConfig(group.id, server.id))
sendMessage("OK已绑定到默认服务器")
} else {
sendMessage("请先绑定群到服务器(使用linkGroup子命令)")
}
} else {
logger.info("启用插件在群 ${group.name}(${group.id})")
g.isEnabled = true
@@ -310,7 +332,13 @@ object PluginCommands : CompositeCommand(
// region 命令别名部分
@SubCommand
@Description("添加命令别名")
@Description("列出所有别名")
suspend fun CommandSender.listCommands() {
sendMessage(PluginConfig.commandAlias.map { "[${it.key}] ${it.value}" }.joinToString())
}
@SubCommand
@Description("添加命令别名,多条命令用|隔开")
suspend fun CommandSender.setCommand(alias: String, vararg command: String) {
if (alias.isEmpty() || command.isEmpty() || command[0].isEmpty()) {
sendMessage("参数不能为空")
@@ -326,5 +354,18 @@ object PluginCommands : CompositeCommand(
sendMessage("OK")
}
@SubCommand
@Description("添加公开命令(游客可执行)(可用别名)")
suspend fun CommandSender.addPublicCommand(command: String) {
PluginConfig.publicCommands.add(command)
sendMessage("OK")
}
@SubCommand
@Description("删除公开命令")
suspend fun CommandSender.removePublicCommand(alias: String) {
PluginConfig.publicCommands.remove(alias)
sendMessage("OK")
}
// endregion
}

View File

@@ -36,17 +36,26 @@ object PluginConfig : AutoSavePluginConfig("config") {
@ValueDescription("命令别名")
val commandAlias: MutableMap<String, String> by value(mutableMapOf(
"无敌" to "/prop god on",
"关闭无敌" to "/prop god off",
"无限体力" to "/prop ns on",
"关闭无限体力" to "/prop ns off",
"无限能量" to "/prop ue on",
"关闭无限能量" to "/prop ue off",
"点亮地图" to "/prop unlockmap 1",
"解锁地图" to "/prop unlockmap 1",
"位置" to "/pos",
"坐标" to "/pos",
"无敌" to "prop god on",
"关闭无敌" to "prop god off",
"无限体力" to "prop ns on",
"关闭无限体力" to "prop ns off",
"无限能量" to "prop ue on",
"关闭无限能量" to "prop ue off",
"点亮地图" to "prop unlockmap 1",
"解锁地图" to "prop unlockmap 1",
"位置" to "pos",
"坐标" to "pos",
// TODO ...
))
@ValueDescription("公开命令,无需绑定账号也可以执行(可用别名)(必须绑定了控制台令牌才可使用)")
val publicCommands: MutableSet<String> by value(mutableSetOf(
"list", "list uid"
))
@ValueDescription("默认服务器ID未指定服务器ID的命令将使用默认服务器执行。\n" +
"私聊默认使用该服务器。")
var defaultServerId: Int by value(1)
}

View File

@@ -26,13 +26,13 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import top.jie65535.mirai.utils.UnsafeOkHttpClient
@OptIn(ExperimentalSerializationApi::class)
object OpenCommandApi {
private val httpClient = OkHttpClient.Builder().build()
private val httpClient = UnsafeOkHttpClient.getUnsafeOkHttpClient().build()
private val json = Json {
ignoreUnknownKeys = true
encodeDefaults = true
@@ -140,13 +140,47 @@ object OpenCommandApi {
/**
* 运行命令成功时返回命令执行结果失败时抛出异常异常详情参考doRequest描述
* 允许单次执行多条命令,用换行(\n)分隔
* @param host 服务器地址
* @param token 持久令牌
* @param command 命令行
* @param rawCommands 命令行
* @return 命令执行结果
* @see doRequest
*/
suspend fun runCommand(host: String, token: String, command: String): String? {
return doRequest(host, json.encodeToString(CommandRequest(token, command)))
suspend fun runCommands(host: String, token: String, rawCommands: String): String {
// 去除首尾空白、命令前缀。使用api执行命令不需要前缀
val commands = rawCommands.splitToSequence('\n')
.map { it.trim().trimStart('/').trimStart('!') }
.toList()
return if (commands.isEmpty())
throw IllegalArgumentException("命令不能为空!")
else if (commands.size == 1) {
val ret = doRequest(host, json.encodeToString(CommandRequest(token, commands[0])))
if (ret.isNullOrEmpty()) "OK" else ret
} else {
val msg = StringBuilder()
var okCount = 0
for (cmd in commands) {
val ret = doRequest(host, json.encodeToString(CommandRequest(token, cmd)))
if (ret.isNullOrEmpty()) {
if (okCount++ == 0)
msg.append("OK")
} else {
if (okCount > 0) {
if (okCount > 1) {
msg.append('*').append(okCount)
}
msg.appendLine()
okCount = 0
}
msg.appendLine(ret)
}
}
if (okCount > 1) // OK*n
msg.append('*').append(okCount)
else if (msg[msg.length-1] == '\n') // 移除额外的换行
msg.deleteCharAt(msg.length-1)
msg.toString()
}
}
}

View File

@@ -0,0 +1,44 @@
package top.jie65535.mirai.utils
import okhttp3.OkHttpClient
import java.security.cert.CertificateException
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
class UnsafeOkHttpClient {
companion object {
fun getUnsafeOkHttpClient(): OkHttpClient.Builder {
try {
// Create a trust manager that does not validate certificate chains
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
@Throws(CertificateException::class)
override fun checkClientTrusted(chain: Array<java.security.cert.X509Certificate>, authType: String) {
}
@Throws(CertificateException::class)
override fun checkServerTrusted(chain: Array<java.security.cert.X509Certificate>, authType: String) {
}
override fun getAcceptedIssuers(): Array<java.security.cert.X509Certificate> {
return arrayOf()
}
})
// Install the all-trusting trust manager
val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, trustAllCerts, java.security.SecureRandom())
// Create an ssl socket factory with our all-trusting manager
val sslSocketFactory = sslContext.socketFactory
val builder = OkHttpClient.Builder()
builder.sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager)
// builder.hostnameVerifier { _, _ -> true }
builder.hostnameVerifier { _, _ -> true }
return builder
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
}