00ll00 vor 5 Jahren
Ursprung
Commit
a8e163ddff

+ 393 - 0
src/main/kotlin/bot_luo_core/cli/commands/DataCmd.kt

@@ -0,0 +1,393 @@
+package bot_luo_core.cli.commands
+
+import bot_luo_core.cli.*
+import bot_luo_core.cli.annotation.Argument
+import bot_luo_core.cli.annotation.Command
+import bot_luo_core.cli.annotation.Method
+import bot_luo_core.cli.checkers.GroupCmdWorkingChecker
+import bot_luo_core.cli.exceptions.SyntaxError
+import bot_luo_core.cli.handlers.GroupArgHandler
+import bot_luo_core.cli.handlers.JsonPathArgHandler
+import bot_luo_core.cli.handlers.UserArgHandler
+import bot_luo_core.data.Group
+import bot_luo_core.data.User
+import bot_luo_core.data.withLockedAccessing
+import bot_luo_core.util.TableBuilder
+import com.alibaba.fastjson.JSONArray
+import com.alibaba.fastjson.JSONObject
+import kotlin.jvm.Throws
+
+@Command(
+    name = "data",
+    display = "数据",
+    alias = [],
+    usage = "查看或修改数据对象的值",
+    notice = [
+        "此命令涉及数据底层,设置值时请谨慎操作"
+    ]
+)
+class DataCmd(context: CmdContext) : Cmd(context) {
+
+    /*  ========================  get  ========================  */
+
+    @Method(name = "", alias = ["get-group","gg","g"], pmsLevel = CmdPermissionLevel.OP, ignoreCheckers = [GroupCmdWorkingChecker::class])
+    fun getGroup (
+        @Argument(name = "群组", handler = GroupArgHandler::class)
+        group: Group,
+        @Argument(name = "路径", required = false, handler = JsonPathArgHandler::class)
+        pathIn: ArrayList<String>?
+    ): CmdReceipt {
+        val path = pathIn?: ArrayList()
+        val table = TableBuilder(4)
+        table.th("群组数据 —— ${group.name}(${group.id})").br()
+        table.p("/"+path.joinToString("/")).br()
+        try {
+            readByPath(group.jsonObj, path, table)
+            context.print(table.toString())
+            return SUCCESS
+        } catch (e: NullPointerException) {
+            table.tr("元素不存在")
+        } catch (e: IndexOutOfBoundsException) {
+            table.tr("数组下标超出范围")
+        }
+        context.print(table.toString())
+        return FATAL
+    }
+
+    @Method(name = "get-user", alias = ["gu","u"], pmsLevel = CmdPermissionLevel.OP, ignoreCheckers = [GroupCmdWorkingChecker::class])
+    fun getUser (
+        @Argument(name = "用户", handler = UserArgHandler::class)
+        user: User,
+        @Argument(name = "路径", required = false, handler = JsonPathArgHandler::class)
+        pathIn: ArrayList<String>?
+    ): CmdReceipt {
+        val path = pathIn?: ArrayList()
+        val table = TableBuilder(4)
+        table.th("用户数据 —— ${user.name}(${user.id})").br()
+        table.p("/"+path.joinToString("/")).br()
+        try {
+            readByPath(user.jsonObj, path, table)
+            context.print(table.toString())
+            return SUCCESS
+        } catch (e: NullPointerException) {
+            table.tr("元素不存在")
+        } catch (e: IndexOutOfBoundsException) {
+            table.tr("数组下标超出范围")
+        }
+        context.print(table.toString())
+        return FATAL
+    }
+
+    /*  ========================  set  ========================  */
+
+    @Method(name = "set-group", alias = ["sg"], pmsLevel = CmdPermissionLevel.DEBUG, ignoreCheckers = [GroupCmdWorkingChecker::class], order = 0)
+    suspend fun setGroup (
+        @Argument(name = "群组", handler = GroupArgHandler::class)
+        group: Group,
+        @Argument(name = "路径", handler = JsonPathArgHandler::class)
+        path: ArrayList<String>,
+        @Argument(name = "值")
+        value: String
+    ): CmdReceipt {
+        val table = TableBuilder(4)
+        table.th("写入群组数据 —— ${group.name}(${group.id})").br()
+        table.p("/"+path.joinToString("/")).br()
+        try { withLockedAccessing(group) {
+            val (old, new) = writeByPath(group.jsonObj, path, value)
+            table.tr(formatValue(old)).tb("->").tb(formatValue(new))
+            context.print(table.toString())
+            return SUCCESS
+        } } catch (e: NullPointerException) {
+            table.tr("拒绝访问")
+        } catch (e: IndexOutOfBoundsException) {
+            table.tr("数组下标超出范围")
+        }
+        context.print(table.toString())
+        return FATAL
+    }
+
+    @Method(name = "set-user", alias = ["su"], pmsLevel = CmdPermissionLevel.DEBUG, ignoreCheckers = [GroupCmdWorkingChecker::class], order = 0)
+    suspend fun setUser (
+        @Argument(name = "用户", handler = UserArgHandler::class)
+        user: User,
+        @Argument(name = "路径", handler = JsonPathArgHandler::class)
+        path: ArrayList<String>,
+        @Argument(name = "值")
+        value: String
+    ): CmdReceipt {
+        val table = TableBuilder(4)
+        table.th("写入用户数据 —— ${user.name}(${user.id})").br()
+        table.p("/"+path.joinToString("/")).br()
+        try { withLockedAccessing(user) {
+            val (old, new) = writeByPath(user.jsonObj, path, value)
+            table.tr(formatValue(old)).tb("->").tb(formatValue(new))
+            context.print(table.toString())
+            return SUCCESS
+        } } catch (e: NullPointerException) {
+            table.tr("拒绝访问")
+        } catch (e: IndexOutOfBoundsException) {
+            table.tr("数组下标超出范围")
+        }
+        context.print(table.toString())
+        return FATAL
+    }
+
+    @Method(name = "set-group", alias = ["sg"], pmsLevel = CmdPermissionLevel.DEBUG, ignoreCheckers = [GroupCmdWorkingChecker::class], order = 1)
+    suspend fun setGroup (
+        @Argument(name = "群组", handler = GroupArgHandler::class)
+        group: Group,
+        @Argument(name = "路径", handler = JsonPathArgHandler::class)
+        path: ArrayList<String>,
+        @Argument(name = "值")
+        value: String,
+        @Suppress("UNUSED_PARAMETER")
+        @Argument(name = "as", literal = true)
+        ignore: Any,
+        @Argument(name = "类型")
+        type: String
+    ): CmdReceipt {
+        val table = TableBuilder(4)
+        table.th("写入群组数据 —— ${group.name}(${group.id})").br()
+        table.p("/"+path.joinToString("/")).br()
+        try { withLockedAccessing(group) {
+            val (old, new) = writeByPathTyped(group.jsonObj, path, value castTo type)
+            table.tr(formatValue(old)).tb("->").tb(formatValue(new))
+            context.print(table.toString())
+            return SUCCESS
+        } } catch (e: NullPointerException) {
+            table.tr("拒绝访问")
+        } catch (e: IndexOutOfBoundsException) {
+            table.tr("数组下标超出范围")
+        }
+        context.print(table.toString())
+        return FATAL
+    }
+
+    @Method(name = "set-user", alias = ["su"], pmsLevel = CmdPermissionLevel.DEBUG, ignoreCheckers = [GroupCmdWorkingChecker::class], order = 1)
+    suspend fun setUser (
+        @Argument(name = "用户", handler = UserArgHandler::class)
+        user: User,
+        @Argument(name = "路径", handler = JsonPathArgHandler::class)
+        path: ArrayList<String>,
+        @Argument(name = "值")
+        value: String,
+        @Suppress("UNUSED_PARAMETER")
+        @Argument(name = "as", literal = true)
+        ignore: Any,
+        @Argument(name = "类型")
+        type: String
+    ): CmdReceipt {
+        val table = TableBuilder(4)
+        table.th("写入用户数据 —— ${user.name}(${user.id})").br()
+        table.p("/"+path.joinToString("/")).br()
+        try { withLockedAccessing(user) {
+            val (old, new) = writeByPathTyped(user.jsonObj, path, value castTo type)
+            table.tr(formatValue(old)).tb("->").tb(formatValue(new))
+            context.print(table.toString())
+            return SUCCESS
+        } } catch (e: NullPointerException) {
+            table.tr("拒绝访问")
+        } catch (e: IndexOutOfBoundsException) {
+            table.tr("数组下标超出范围")
+        }
+        context.print(table.toString())
+        return FATAL
+    }
+
+    @Suppress("UNCHECKED_CAST")
+    companion object {
+
+        private infix fun String.castTo(type: String): Any? {
+            return when (type.lowercase()) {
+                "num","number","float","double" -> this.toBigDecimalOrNull()?: throw SyntaxError(-1, "无法将“$this”转换为浮点数")
+                "int","integer","short","long","longlong" -> this.toBigIntegerOrNull()?: throw SyntaxError(-1, "无法将“$this”转换为整数")
+                "str","string","charsequence" -> this
+                "bool","boolean" -> this.toBoolean()
+                "null" -> if (this == "null") return null else throw SyntaxError(-1, "无法将“$this”转换为Null")
+                "array","list" -> if (this == "[]") return JSONArray() else throw SyntaxError(-1, "无法将“$this”转换为Array")
+                "object","obj" -> if (this == "{}") return JSONObject() else throw SyntaxError(-1, "无法将“$this”转换为Object")
+                else -> throw SyntaxError(-1, "未知的类型“$type”")
+            }
+        }
+
+        private fun objectGet(obj: JSONObject, table: TableBuilder) {
+            for ((k, v) in obj.innerMap) {
+                table.tr("$k:").tb(formatValue(v))
+            }
+        }
+
+        private fun arrayGet(array: JSONArray, table: TableBuilder) {
+            for (i in array.indices) {
+                table.tr("[$i]").tb(formatValue(array[i]))
+            }
+        }
+
+        private fun formatValue(value: Any?): String {
+            return when (value) {
+                is Boolean -> value.toString()
+                is Number -> value.toString()
+                is String -> "\"$value\""
+                is Map<*, *> -> "<object>"
+                is List<*> -> "<array>"
+                null -> "null"
+                else -> "<unk>"
+            }
+        }
+
+        @Throws(IndexOutOfBoundsException::class, NullPointerException::class)
+        private fun readByPath(obj: Any?, path: ArrayList<String> = ArrayList(), table: TableBuilder) {
+            if (path.isEmpty()) {
+                when (obj) {
+                    is Boolean -> table.tr(obj)
+                    is Number -> table.tr(obj)
+                    is String -> table.tr("\"$obj\"")
+                    is JSONObject -> objectGet(obj, table)
+                    is JSONArray -> arrayGet(obj, table)
+                    null -> throw NullPointerException()
+                    else -> table.tr("<unk>")
+                }
+            } else {
+                when (obj) {
+                    is JSONObject -> {
+                        val key = path[0]
+                        if (!obj.containsKey(key)) throw NullPointerException()
+                        readByPath(obj[key], path.apply { removeAt(0) }, table)
+                    }
+                    is JSONArray -> {
+                        val index = path[0].toIntOrNull() ?: throw NullPointerException()
+                        if (index !in obj.indices) throw IndexOutOfBoundsException()
+                        readByPath(obj[index], path.apply { removeAt(0) }, table)
+                    }
+                    else -> throw NullPointerException()
+                }
+            }
+        }
+
+        /**
+         * 写入数据
+         *
+         * 若此路径已存在值,则根据旧值类型转换新值,转换失败则写入[String]
+         *
+         * 返回 [Pair] (旧数据,新数据)
+         */
+        @Throws(NullPointerException::class)
+
+        private fun writeByPath(obj: Any?, path: ArrayList<String>, value: String): Pair<Any?,Any> {
+            when (path.size) {
+                0 -> throw NullPointerException()
+                1 -> {
+                    when (obj) {
+                        is MutableMap<*, *> -> {
+                            obj as MutableMap<String, Any?>
+                            val oldValue = obj[path[0]]
+                            when (obj[path[0]]) {
+                                is String -> obj[path[0]] = value
+                                is Number -> obj[path[0]] = value.toBigDecimalOrNull()?: value
+                                is Boolean -> obj[path[0]] = value.toBooleanStrictOrNull()?: value
+                                null -> obj[path[0]] = value
+                                else -> throw NullPointerException()
+                            }
+                            return oldValue to obj[path[0]]!!
+                        }
+                        is MutableList<*> -> {
+                            obj as MutableList<Any>
+                            val index = path[0].toIntOrNull()?: throw NullPointerException()
+                            if (index !in 0..obj.size) throw IndexOutOfBoundsException()
+                            val oldValue: Any?
+                            if (index == obj.size) {
+                                oldValue = null
+                                obj.add(value)
+                            } else {
+                                oldValue = obj[index]
+                                when (obj[index]) {
+                                    is String -> obj[index] = value
+                                    is Number -> obj[index] = value.toBigDecimalOrNull() ?: value
+                                    is Boolean -> obj[index] = value.toBooleanStrictOrNull() ?: value
+                                    else -> throw NullPointerException()
+                                }
+                            }
+                            return oldValue to obj[index]
+                        }
+                        else -> throw NullPointerException()
+                    }
+                }
+                else -> {
+                    return when (obj) {
+                        is JSONObject -> {
+                            val key = path[0]
+                            if (!obj.containsKey(key)) throw NullPointerException()
+                            writeByPath(obj[key], path.apply { removeAt(0) }, value)
+                        }
+                        is JSONArray -> {
+                            val index = path[0].toIntOrNull() ?: throw NullPointerException()
+                            if (index !in obj.indices) throw IndexOutOfBoundsException()
+                            writeByPath(obj[index], path.apply { removeAt(0) }, value)
+                        }
+                        else -> throw NullPointerException()
+                    }
+                }
+            }
+        }
+
+        /**
+         * 写入数据
+         *
+         * 不进行类型转换
+         *
+         * 返回 [Pair] (旧数据,新数据)
+         */
+        @Throws(NullPointerException::class)
+        private fun writeByPathTyped(obj: Any?, path: ArrayList<String>, value: Any?): Pair<Any?,Any?> {
+            when (path.size) {
+                0 -> throw NullPointerException()
+                1 -> {
+                    when (obj) {
+                        is MutableMap<*, *> -> {
+                            obj as MutableMap<String, Any?>
+                            val oldValue = obj[path[0]]
+                            if (value == null)
+                                obj.remove(path[0])
+                            else
+                                obj[path[0]] = value
+                            return oldValue to obj[path[0]]
+                        }
+                        is MutableList<*> -> {
+                            obj as MutableList<Any>
+                            val index = path[0].toIntOrNull()?: throw NullPointerException()
+                            if (index !in 0..obj.size) throw IndexOutOfBoundsException()
+                            val oldValue: Any?
+                            if (index == obj.size) {
+                                oldValue = null
+                                if (value != null)
+                                    obj.add(value)
+                            } else {
+                                oldValue = obj[index]
+                                if (value == null)
+                                    obj.removeAt(index)
+                                else
+                                    obj[index] = value
+                            }
+                            return oldValue to if (value == null) null else obj[index]
+                        }
+                        else -> throw NullPointerException()
+                    }
+                }
+                else -> {
+                    return when (obj) {
+                        is JSONObject -> {
+                            val key = path[0]
+                            if (!obj.containsKey(key)) throw NullPointerException()
+                            writeByPathTyped(obj[key], path.apply { removeAt(0) }, value)
+                        }
+                        is JSONArray -> {
+                            val index = path[0].toIntOrNull() ?: throw NullPointerException()
+                            if (index !in obj.indices) throw IndexOutOfBoundsException()
+                            writeByPathTyped(obj[index], path.apply { removeAt(0) }, value)
+                        }
+                        else -> throw NullPointerException()
+                    }
+                }
+            }
+        }
+    }
+}

+ 66 - 0
src/main/kotlin/bot_luo_core/cli/handlers/JsonPathArgHandler.kt

@@ -0,0 +1,66 @@
+package bot_luo_core.cli.handlers
+
+import bot_luo_core.cli.ArgHandler
+import bot_luo_core.cli.CmdContext
+import bot_luo_core.cli.MessageReader
+import bot_luo_core.cli.exceptions.HandlerFatal
+import bot_luo_core.cli.exceptions.SyntaxError
+import kotlin.reflect.KType
+
+class JsonPathArgHandler: ArgHandler<ArrayList<String>> {
+
+    override val name = "JSON路径参数解析器"
+
+    override fun handle(
+        reader: MessageReader,
+        pos: Int,
+        argName: String?,
+        type: KType?,
+        context: CmdContext?
+    ): ArrayList<String> {
+        val res = ArrayList<String>()
+        val sb = StringBuilder()
+        var open = false
+        while (reader.canRead() && !reader.isWhitespace(reader.peek())) {
+            when(reader.peek()) {
+                in 'a'..'z',
+                in 'A'..'Z',
+                '-','_'-> {
+                    if (open) throw SyntaxError(reader.getCursor(),"期望一个数字 : ${reader.getCursor()}")
+                    sb.append(reader.read())
+                }
+                in '0'..'9' -> sb.append(reader.read())
+                '.' -> {
+                    if (open) throw SyntaxError(reader.getCursor(),"'['未闭合 : ${reader.getCursor()}")
+                    if (sb.isEmpty()) throw SyntaxError(reader.getCursor(),"期望一个文本 : ${reader.getCursor()}")
+                    res.add(sb.toString())
+                    sb.clear()
+                    reader.skip()
+                    if (reader.canRead() && (reader.isWhitespace(reader.peek()) || reader.peek() == '['))
+                        throw SyntaxError(reader.getCursor(),"期望一个文本 : ${reader.getCursor()}")
+                }
+                '[' -> {
+                    if (open) throw SyntaxError(reader.getCursor(),"不能在索引中使用'[' : ${reader.getCursor()}")
+                    open = true
+                    if (sb.isNotEmpty()) {
+                        res.add(sb.toString())
+                        sb.clear()
+                    }
+                    reader.skip()
+                }
+                ']' -> {
+                    if (!open) throw SyntaxError(reader.getCursor(), "']'无对应的'[' : ${reader.getCursor()}")
+                    if (sb.isEmpty()) throw SyntaxError(reader.getCursor(), "索引值不能为空 : ${reader.getCursor()}")
+                    open = false
+                    reader.skip()
+                    if (reader.canRead() && !reader.isWhitespace(reader.peek()) && reader.peek() != '.')
+                        throw SyntaxError(reader.getCursor(), "期望一个'.' : ${reader.getCursor()}")
+                }
+                else -> throw SyntaxError(reader.getCursor(),"无法解析的字符'${reader.peek()}' : ${reader.getCursor()}")
+            }
+        }
+        if (open) throw SyntaxError(reader.getCursor(),"'['未闭合 : ${reader.getCursor()}")
+        if (sb.isNotEmpty()) res.add(sb.toString())
+        return res
+    }
+}

+ 19 - 0
src/main/kotlin/bot_luo_core/cli/handlers/LiteralArgHandler.kt

@@ -0,0 +1,19 @@
+package bot_luo_core.cli.handlers
+
+import bot_luo_core.cli.ArgHandler
+import bot_luo_core.cli.CmdContext
+import bot_luo_core.cli.MessageReader
+import bot_luo_core.cli.exceptions.CliInternalError
+import bot_luo_core.cli.exceptions.HandlerFatal
+import kotlin.reflect.KType
+
+class LiteralArgHandler: ArgHandler<Unit> {
+
+    override val name = "字面量参数解析器"
+
+    override fun handle(reader: MessageReader, pos: Int, argName: String?, type: KType?, context: CmdContext?) {
+        argName?: throw CliInternalError("缺少字面量名称")
+        val input = reader.readUnquotedString()
+        if (input != argName) throw HandlerFatal(input, argName, pos, type)
+    }
+}