Initial commit

This commit is contained in:
2022-09-04 14:07:47 +08:00
parent 10fa82b900
commit ea83aec168
11 changed files with 1253 additions and 15 deletions

View File

@@ -1,8 +1,40 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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.utils.info
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(
@@ -15,6 +47,218 @@ object JGrasscutterCommand : KotlinPlugin(
}
) {
override fun onEnable() {
logger.info { "Plugin loaded" }
PluginConfig.reload()
PluginData.reload()
PluginCommands.register()
val eventChannel = GlobalEventChannel.parentScope(this)
// 监听群消息
eventChannel.subscribeAlways<GroupMessageEvent> {
// 忽略被拉黑的用户发送的消息
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 <color=green>${sender.nameCardOrNick}</color>:\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<MessageEvent>(priority = EventPriority.HIGH) { event ->
// 仅监听该用户的消息并且消息内容为4位数字
event.sender.id == user.id && event.message.firstIsInstanceOrNull<PlainText>()
?.content?.trim()
?.toIntOrNull()
?.let { it in 1000..9999 } == true
}
// 得到消息中的4位数字代码
val code = nextEvent.message.firstIsInstance<PlainText>().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!!)
}
}
}

View File

@@ -1,4 +1,330 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package top.jie65535.mirai
object PluginCommands {
import net.mamoe.mirai.console.command.*
import net.mamoe.mirai.contact.*
import top.jie65535.mirai.JGrasscutterCommand.reload
import top.jie65535.mirai.model.GroupConfig
import top.jie65535.mirai.model.Server
import top.jie65535.mirai.opencommand.OpenCommandApi
object PluginCommands : CompositeCommand(
JGrasscutterCommand, "jgc",
description = "管理插件设置"
) {
private val logger = JGrasscutterCommand.logger
@SubCommand
@Description("插件命令用法")
suspend fun CommandSender.help() {
sendMessage(usage)
}
@SubCommand
@Description("重载插件配置")
suspend fun CommandSender.reload() {
logger.info("重载插件配置")
PluginConfig.reload()
sendMessage("OK")
}
@SubCommand
@Description("设置执行GC命令前缀")
suspend fun CommandSender.setCommandPrefix(prefix: String) {
if (prefix.isEmpty()) {
sendMessage("前缀不能为空,这会导致每条消息都作为命令处理")
} else {
logger.info("设置执行GC命令前缀为 $prefix")
PluginConfig.commandPrefix = prefix
sendMessage("OK")
}
}
@SubCommand
@Description("设置绑定命令前缀")
suspend fun CommandSender.setBindCommand(prefix: String) {
if (prefix.isEmpty()) {
sendMessage("不能为空")
} else {
logger.info("设置绑定命令为 $prefix")
PluginConfig.bindCommand = prefix
sendMessage("OK")
}
}
@SubCommand("setAdmin", "op")
@Description("设置管理员")
suspend fun CommandSender.setAdmin(user: User) {
logger.info("添加管理员 ${user.nameCardOrNick}(${user.id})")
PluginConfig.administrators.add(user.id)
sendMessage("OK")
}
@SubCommand("removeAdmin", "deop")
@Description("解除管理员")
suspend fun CommandSender.removeAdmin(user: User) {
PluginConfig.administrators.remove(user.id)
logger.info("解除管理员 ${user.nameCardOrNick}(${user.id})")
sendMessage("OK")
}
@SubCommand
@Description("禁止指定QQ使用插件")
suspend fun CommandSender.ban(qq: Long) {
logger.info("禁止${qq}使用插件")
PluginConfig.blacklist.add(qq)
sendMessage("OK")
}
@SubCommand
@Description("解除禁止指定QQ使用插件")
suspend fun CommandSender.unban(qq: Long) {
logger.info("解除禁止${qq}使用插件")
PluginConfig.blacklist.remove(qq)
sendMessage("OK")
}
// region 服务器相关命令
@SubCommand
@Description("测试指定服务器是否安装插件")
suspend fun CommandSender.ping(address: String) {
val id = address.toIntOrNull()
val serverAddress = if (id != null) {
val server = PluginData.servers.find { it.id == id }
if (server == null) {
sendMessage("未找到指定服务器")
return
} else {
server.address
}
} else {
address
}
if (tryPing(serverAddress)) {
sendMessage("OK")
} else {
sendMessage("Error")
}
}
private suspend fun tryPing(address: String): Boolean {
return try {
logger.info("正在 ping $address")
OpenCommandApi.ping(address)
true
} catch (e: Throwable) {
logger.warning("ping $address 异常", e)
false
}
}
@SubCommand
@Description("添加服务器")
suspend fun CommandSender.addServer(address: String, name: String = "", vararg description: String = arrayOf()) {
if (address.isEmpty()) {
sendMessage("服务器地址不能为空!")
return
}
val descriptionStr = description.joinToString(" ")
if (tryPing(address)) {
logger.info("添加服务器:$address\tname=$name\tdescription=$descriptionStr")
val serverId = ++PluginData.lastServerId
PluginData.servers.add(Server(serverId, address, name, descriptionStr))
sendMessage("服务器已添加ID为[$serverId]使用servers子命令查看服务器列表")
} else {
sendMessage("只能设置装有 [OpenCommand](https://github.com/jie65535/gc-opencommand-plugin) 插件的服务器")
}
}
@SubCommand
@Description("列出服务器")
suspend fun CommandSender.servers() {
if (PluginData.servers.isEmpty()) {
sendMessage("服务器列表为空使用addServer子命令来添加服务器")
} else {
sendMessage(PluginData.servers.joinToString("\n") {
if (it.description.isNotEmpty())
"[${it.id}] ${it.name} ${it.address}\n${it.description}"
else
"[${it.id}] ${it.name} ${it.address}"
})
}
}
@SubCommand
@Description("设置服务器启用")
suspend fun CommandSender.setServerIsEnabled(id: Int, isEnabled: Boolean) {
val server = PluginData.servers.find { it.id == id }
if (server == null) {
sendMessage("未找到指定服务器")
} else {
logger.info("${if (isEnabled) "启用" else "禁用"}服务器[$id]")
server.isEnabled = isEnabled
sendMessage("OK")
}
}
@SubCommand
@Description("修改服务器地址")
suspend fun CommandSender.setServerAddress(id: Int, address: String) {
val server = PluginData.servers.find { it.id == id }
if (server == null) {
sendMessage("未找到指定服务器")
} else {
if (tryPing(address)) {
logger.info("修改服务器地址为:$address")
server.address = address
sendMessage("OK")
} else {
sendMessage("只能设置装有 [OpenCommand](https://github.com/jie65535/gc-opencommand-plugin) 插件的服务器")
}
}
}
@SubCommand
@Description("设置服务器信息")
suspend fun CommandSender.setServerInfo(id: Int, name: String, vararg description: String) {
val server = PluginData.servers.find { it.id == id }
if (server == null) {
sendMessage("未找到指定服务器")
} else {
val descriptionStr = description.joinToString(" ")
logger.info("设置服务器信息为name=$name\tdescription=$descriptionStr")
server.name = name
server.description = descriptionStr
sendMessage("OK")
}
}
@SubCommand
@Description("设置服务器控制台令牌")
suspend fun CommandSender.setServerConsoleToken(id: Int, consoleToken: String) {
val server = PluginData.servers.find { it.id == id }
if (server == null) {
sendMessage("未找到指定服务器")
} else {
logger.info("设置服务器控制台令牌为:$consoleToken")
server.consoleToken = consoleToken
sendMessage("OK")
}
}
@SubCommand
@Description("设置是否同步群消息到服务器")
suspend fun CommandSender.setServerSyncMessage(id: Int, sync: Boolean) {
val server = PluginData.servers.find { it.id == id }
if (server == null) {
sendMessage("未找到指定服务器")
} else {
logger.info("服务器[$id]${if (sync) "启用" else "禁用"}消息同步")
server.syncMessage = sync
sendMessage("OK")
}
}
// endregion 服务器相关命令
// region 群相关命令
@SubCommand("linkGroup", "bindGroup", "addGroup")
@Description("绑定服务器到群")
suspend fun CommandSender.linkGroup(serverId: Int, group: Group? = getGroupOrNull()) {
if (group == null) {
sendMessage("必须指定群")
return
}
val server = PluginData.servers.find { it.id == serverId }
if (server == null) {
sendMessage("指定服务器ID不存在请先添加服务器(使用addServer子命令)")
return
}
logger.info("将服务器[$serverId] ${server.name} ${server.address} 绑定到群 ${group.name}(${group.id})")
val g = PluginData.groups.find { it.id == group.id }
if (g == null) {
PluginData.groups.add(GroupConfig(group.id, serverId))
sendMessage("OK默认已启用")
} else {
g.serverId = serverId
sendMessage("OK")
}
}
@SubCommand
@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子命令)")
} else {
logger.info("启用插件在群 ${group.name}(${group.id})")
g.isEnabled = true
sendMessage("OK")
}
}
}
@SubCommand
@Description("禁用指定群执行")
suspend fun CommandSender.disable(group: Group? = getGroupOrNull()) {
if (group == null) {
sendMessage("必须指定群")
} else {
val g = PluginData.groups.find { it.id == group.id }
if (g == null) {
sendMessage("请先绑定群到服务器(使用linkGroup子命令)")
} else {
logger.info("禁用插件在群 ${group.name}(${group.id})")
g.isEnabled = false
sendMessage("OK")
}
}
}
// endregion 群相关命令
// region 命令别名部分
@SubCommand
@Description("添加命令别名")
suspend fun CommandSender.setCommand(alias: String, vararg command: String) {
if (alias.isEmpty() || command.isEmpty() || command[0].isEmpty()) {
sendMessage("参数不能为空")
}
PluginConfig.commandAlias[alias] = command.joinToString(" ")
sendMessage("OK")
}
@SubCommand
@Description("删除命令别名")
suspend fun CommandSender.removeCommand(alias: String) {
PluginConfig.commandAlias.remove(alias)
sendMessage("OK")
}
// endregion
}

View File

@@ -1,4 +1,52 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package top.jie65535.mirai
object PluginConfig {
import net.mamoe.mirai.console.data.*
object PluginConfig : AutoSavePluginConfig("config") {
@ValueDescription("管理员列表,仅管理员可以执行控制台命令")
val administrators: MutableSet<Long> by value()
@ValueDescription("用户黑名单")
val blacklist: MutableSet<Long> by value()
@ValueDescription("绑定命令:绑定 <UID> 示例:绑定 10001")
var bindCommand: String by value("绑定")
@ValueDescription("聊天中执行GC命令前缀!<命令|别名>\n" +
"示例1!give 1096 lv90\n" +
"示例2!位置\n")
var commandPrefix: String by value("!")
@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",
// TODO ...
))
}

View File

@@ -1,4 +1,40 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package top.jie65535.mirai
object PluginData {
import net.mamoe.mirai.console.data.AutoSavePluginData
import net.mamoe.mirai.console.data.ValueDescription
import net.mamoe.mirai.console.data.value
import top.jie65535.mirai.model.GroupConfig
import top.jie65535.mirai.model.Server
import top.jie65535.mirai.model.User
object PluginData : AutoSavePluginData("data") {
@ValueDescription("服务器列表")
val servers: MutableList<Server> by value()
@ValueDescription("最后服务器ID用于递增")
var lastServerId: Int by value()
@ValueDescription("玩家列表")
val users: MutableList<User> by value()
@ValueDescription("群列表")
val groups: MutableList<GroupConfig> by value()
}

View File

@@ -1,3 +1,20 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package top.jie65535.mirai.model
import kotlinx.serialization.Serializable
@@ -6,7 +23,7 @@ import kotlinx.serialization.Serializable
* 群类型
*/
@Serializable
data class Group(
data class GroupConfig(
/**
* 群IDQQ群号
*/
@@ -20,5 +37,5 @@ data class Group(
/**
* 是否启用(用于临时关闭)
*/
var enabled: Boolean = true,
var isEnabled: Boolean = true,
)

View File

@@ -1,10 +1,62 @@
package top.jie65535.mirai
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package top.jie65535.mirai.model
import kotlinx.serialization.Serializable
/**
* 服务器类型
*/
@Serializable
data class ServerConfig(
data class Server(
/**
* 服务器ID
* 自动递增
*/
val id: Int,
/**
* 服务器地址
*/
var address: String,
var consoleToken: String,
/**
* 服务器名称
*/
var name: String = "",
/**
* 服务器说明
*/
var description: String = "",
/**
* 控制台令牌
*/
var consoleToken: String = "",
/**
* 服务器是否已启用
*/
var isEnabled: Boolean = true,
/**
* 同步群消息到服务器,必须设置了控制台令牌
*/
var syncMessage: Boolean = false,
)

View File

@@ -1,4 +1,62 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package top.jie65535.mirai.model
class User {
import kotlinx.serialization.Serializable
import top.jie65535.mirai.serializers.LocalDateTimeSerializer
import java.time.LocalDateTime
@Serializable
data class User(
/**
* 用户IDQQ帐号
*/
val id: Long,
/**
* 服务器ID
*/
val serverId: Int,
/**
* 游戏UID
*/
var uid: Int,
) {
/**
* 令牌,失效时清空
*/
var token: String = ""
/**
* 用户添加时间
*/
@Serializable(LocalDateTimeSerializer::class)
val createTime: LocalDateTime = LocalDateTime.now()
/**
* 运行命令计数
*/
var runCount: Int = 0
/**
* 最后运行时间
*/
@Serializable(LocalDateTimeSerializer::class)
var lastRunTime: LocalDateTime? = null
}

View File

@@ -1,4 +1,152 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package top.jie65535.mirai.opencommand
object OCApi {
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
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
@OptIn(ExperimentalSerializationApi::class)
object OpenCommandApi {
private val httpClient = OkHttpClient.Builder().build()
private val json = Json {
ignoreUnknownKeys = true
encodeDefaults = true
}
@Serializable
private data class PingRequest(
val action: String = "ping"
)
@Serializable
private data class SendCodeRequest(
@SerialName("data")
val uid: Int,
val action: String = "sendCode"
)
@Serializable
private data class VerifyRequest(
val token: String,
@SerialName("data")
val code: Int,
val action: String = "verify"
)
@Serializable
private data class CommandRequest(
val token: String,
@SerialName("data")
val command: String,
val action: String = "command",
)
@Serializable
private data class StringResponse(
val retcode: Int,
val message: String,
val data: String?
)
class HttpException(val code: Int, message: String) : Exception(message)
class InvokeException(val code: Int, message: String) : Exception(message)
private val JSON = "application/json; charset=utf-8".toMediaType()
/**
* 向服务器发起opencommand请求
* @param host 服务器地址
* @param jsonBody 请求正文Json
* @return 如果一切正常,返回相应数据,可能为空
* @exception InvokeException HTTP请求执行成功但opencommand插件调用失败
* @exception HttpException HTTP请求完成但HTTP响应错误代码例如404、500等
* @exception IOException 如果由于取消、连接问题或超时而无法执行请求。由于网络可能在交换期间发生故障,因此远程服务器可能在故障之前接受了请求
*/
private suspend fun doRequest(host: String, jsonBody: String): String? {
val api = "$host/opencommand/api"
val request = Request.Builder()
.url(api)
.post(jsonBody.toRequestBody(JSON))
.build()
// JGrasscutterCommand.logger.debug("POST to $api Body $jsonBody")
return withContext(Dispatchers.IO) {
httpClient.newCall(request).execute().use { httpResponse ->
val responseBody = httpResponse.body
if (httpResponse.code == 200 && responseBody != null) {
val response = json.decodeFromStream<StringResponse>(responseBody.byteStream())
if (response.retcode != 200) {
throw InvokeException(response.retcode, response.data ?: response.message)
} else {
return@use response.data
}
} else {
throw HttpException(httpResponse.code, httpResponse.message)
}
}
}
}
/**
* 测试链接
* @param host 服务器地址
*/
suspend fun ping(host: String) {
doRequest(host, json.encodeToString(PingRequest()))
}
/**
* 发送验证码
* @param host 服务器地址
* @param uid 目标玩家UID
* @return 返回临时Token用于验证
*/
suspend fun sendCode(host: String, uid: Int): String {
return doRequest(host, json.encodeToString(SendCodeRequest(uid)))!!
}
/**
* 验证身份验证失败时将抛出异常异常详情参考doRequest描述
* @param host 服务器地址
* @param token 发送验证码时返回的临时令牌
* @param code 用户输入的验证代码
* @see doRequest
*/
suspend fun verify(host: String, token: String, code: Int) {
doRequest(host, json.encodeToString(VerifyRequest(token, code)))
}
/**
* 运行命令成功时返回命令执行结果失败时抛出异常异常详情参考doRequest描述
* @param host 服务器地址
* @param token 持久令牌
* @param command 命令行
* @return 命令执行结果
* @see doRequest
*/
suspend fun runCommand(host: String, token: String, command: String): String? {
return doRequest(host, json.encodeToString(CommandRequest(token, command)))
}
}

View File

@@ -1,4 +1,38 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package top.jie65535.mirai.serializers
class LocalDateTimeSerializer {
}
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import java.time.LocalDateTime
@OptIn(ExperimentalSerializationApi::class)
@Serializer(LocalDateTime::class)
object LocalDateTimeSerializer: KSerializer<LocalDateTime> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): LocalDateTime =
LocalDateTime.parse(decoder.decodeString())
override fun serialize(encoder: Encoder, value: LocalDateTime) {
encoder.encodeString(value.toString())
}
}