/*
* JGrasscutterCommand
* 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 .
*/
package top.jie65535.mirai
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withTimeout
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register
import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription
import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin
import net.mamoe.mirai.contact.nameCardOrNick
import net.mamoe.mirai.event.*
import net.mamoe.mirai.event.events.GroupMessageEvent
import net.mamoe.mirai.event.events.MessageEvent
import net.mamoe.mirai.message.data.At
import net.mamoe.mirai.message.data.MessageSource.Key.quote
import net.mamoe.mirai.message.data.PlainText
import net.mamoe.mirai.message.data.firstIsInstance
import net.mamoe.mirai.message.data.firstIsInstanceOrNull
import top.jie65535.mirai.model.User
import top.jie65535.mirai.opencommand.OpenCommandApi
import java.time.LocalDateTime
object JGrasscutterCommand : KotlinPlugin(
JvmPluginDescription(
id = "top.jie65535.mirai.grasscutter-command",
name = "J Grasscutter Command",
version = "0.1.0",
) {
author("jie65535")
info("""聊天执行GC命令""")
}
) {
override fun onEnable() {
PluginConfig.reload()
PluginData.reload()
PluginCommands.register()
val eventChannel = GlobalEventChannel.parentScope(this)
// 监听群消息
eventChannel.subscribeAlways {
// 忽略被拉黑的用户发送的消息
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 = PluginData.servers.find { it.id == groupConfig.serverId }
if (server == null || !server.isEnabled)
return@subscribeAlways
// 解析消息
val message = this.message.joinToString("") {
if (it is At) {
// 替换@群员为@其绑定的Uid
val user = PluginData.users.find { user -> user.id == it.target && user.serverId == server.id }
if (user == null) { it.contentToString() } else { "@${user.uid}" }
} else {
it.contentToString()
}
}
// 处理绑定命令
if (message.startsWith(PluginConfig.bindCommand)) {
val input = message.removePrefix(PluginConfig.bindCommand).trim()
val bindUid = input.toIntOrNull()
if (bindUid == null) {
this.subject.sendMessage(this.message.quote() + "请输入正确的UID")
return@subscribeAlways
}
logger.info("绑定用户 ${sender.nameCardOrNick}(${sender.id}) Uid $bindUid 到服务器 ${server.name} ${server.address}")
var user = PluginData.users.find { it.id == sender.id && it.serverId == server.id }
if (user == null) {
user = User(sender.id, server.id, bindUid)
PluginData.users.add(user)
} else {
if (user.uid != bindUid) {
user.uid = bindUid
// 更换绑定将重置token
user.token = ""
}
}
// 发送验证码,并提示要求回复验证码
sendCode(server.address, user)
}
// 处理执行游戏命令
else if (message.startsWith(PluginConfig.commandPrefix)) {
var command = message.removePrefix(PluginConfig.commandPrefix).trim()
if (command.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 user: User? = null
// 获取token
// 若设置了控制台令牌,则验证是否为管理员执行
val token = if (server.consoleToken.isNotEmpty() && PluginConfig.administrators.contains(sender.id)) {
logger.info("管理员 ${sender.nameCardOrNick}(${sender.id}) 执行命令:$command")
// 设置控制台令牌
server.consoleToken
} else {
// 普通用户
user = PluginData.users.find { it.id == sender.id && it.serverId == server.id }
if (user == null || user.token.isEmpty()) {
return@subscribeAlways
}
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)
}
if (user != null) {
// 计数并更新最后运行时间
++user.runCount
user.lastRunTime = LocalDateTime.now()
}
} catch (e: OpenCommandApi.InvokeException) {
when (e.code) {
404 -> {
subject.sendMessage(this.message.quote() + "玩家不存在或未上线")
}
403 -> {
logger.warning("${sender.nameCardOrNick}(${sender.id}) 的命令执行失败,服务器已收到命令,但不做处理,可能是未验证通过")
// 403不理会用户
}
401 -> {
logger.warning("${sender.nameCardOrNick}(${sender.id}) 的命令执行失败,未授权或已过期的令牌,可以修改插件配置以延长令牌过期时间")
subject.sendMessage(this.message.quote() + "令牌未授权或已过期,请重新绑定账号以更新令牌")
// TODO 此处可以重新发送验证码要求验证,但目前直接报错并要求重新绑定
}
500 -> {
logger.warning("${sender.nameCardOrNick}(${sender.id}) 的命令执行失败,服务器内部错误:${e.message}")
subject.sendMessage(this.message.quote() + "服务器内部发生错误,命令执行失败")
}
else -> {
logger.warning("${sender.nameCardOrNick}(${sender.id}) 的命令执行失败,发生预期外异常:${e.message}")
}
}
} catch (e: Throwable) {
logger.warning("${sender.nameCardOrNick}(${sender.id}) 在执行命令时发生异常", e)
}
}
// 否则如果启用了同步消息,且控制台令牌不为空
else if (server.consoleToken.isNotEmpty() && server.syncMessage) {
try {
OpenCommandApi.runCommand(
server.address,
server.consoleToken,
"say ${sender.nameCardOrNick}:\n${this.message.contentToString()}")
} catch (e: Throwable) {
server.syncMessage = false
logger.warning("同步发送聊天消息失败,自动禁用同步消息,请手动重新启用", e)
}
}
}
logger.info("Plugin loaded")
}
/**
* 发送验证码,并监听用户回复
* @param host 服务器地址
* @param user 用户实例
*/
private suspend fun MessageEvent.sendCode(host: String, user: User) {
try {
logger.info("${sender.nameCardOrNick}(${sender.id}) 正在请求向服务器[${user.serverId}] @${user.uid} 发送验证码")
// 请求发送验证码
user.token = OpenCommandApi.sendCode(host, user.uid)
// 提示用户
subject.sendMessage(message.quote() + "验证码已发送,请在一分钟内将游戏中收到的验证码发送以验证身份。")
// 最多等待1分钟
withTimeout(60 * 1000) {
// 当验证失败时循环重试,最多重试3次
var retry = 3
while (isActive && retry --> 0) {
// 监听消息事件
val nextEvent = GlobalEventChannel.nextEvent(priority = EventPriority.HIGH) { event ->
// 仅监听该用户的消息,并且消息内容为4位数字
event.sender.id == user.id && event.message.firstIsInstanceOrNull()
?.content?.trim()
?.toIntOrNull()
?.let { it in 1000..9999 } == true
}
// 得到消息中的4位数字代码
val code = nextEvent.message.firstIsInstance().content.trim().toInt()
try {
// 请求验证
OpenCommandApi.verify(host, user.token, code)
logger.info("${nextEvent.sender.nameCardOrNick}(${nextEvent.sender.id}) 在服务器[${user.serverId}]验证通过,其游戏Uid为 ${user.uid}")
// 若无异常则验证通过
nextEvent.subject.sendMessage(nextEvent.message.quote() + "验证通过")
// 停止监听
return@withTimeout
} catch (e: OpenCommandApi.InvokeException) {
logger.warning("${nextEvent.sender.nameCardOrNick}(${nextEvent.sender.id}) 在服务器[${user.serverId}] @${user.uid} 验证失败,信息:${e.message}")
// 400为验证失败
if (e.code == 400) {
nextEvent.subject.sendMessage(nextEvent.message.quote() + "验证失败,请重试")
} else {
nextEvent.subject.sendMessage(nextEvent.message.quote() + e.message!!)
}
// 不返回,继续监听
} catch (e: Throwable) {
// 预期外异常,停止监听器
logger.warning("${nextEvent.sender.nameCardOrNick}(${nextEvent.sender.id}) 在向服务器[${user.serverId}] @${user.uid} 发起验证时发生了预期外异常", e)
user.token = ""
nextEvent.subject.sendMessage(nextEvent.message.quote() + "发生内部错误,已取消验证,请重试")
return@withTimeout
}
}
}
} catch (e: OpenCommandApi.InvokeException) {
subject.sendMessage(message.quote() +
when (e.code) {
404 -> "目标玩家不存在或不在线"
403 -> "请求太频繁,请稍后再试"
else -> e.message!!
})
} catch (e: TimeoutCancellationException) {
subject.sendMessage(message.quote() + "等待验证超时,请重试")
} catch (e: Throwable) {
logger.warning("发送验证码出现预期外的异常", e)
if (e.message != null) subject.sendMessage(message.quote() + e.message!!)
}
}
}