فهرست منبع

add ScheduleCmd

00ll00 5 سال پیش
والد
کامیت
80934d054d

+ 6 - 3
src/main/kotlin/bot_luo_core/bot/BotLuo.kt

@@ -4,13 +4,12 @@ import bot_luo_core.cli.CmdHandler
 import bot_luo_core.cli.commands.NoticeCmd
 import bot_luo_core.data.Data
 import bot_luo_core.data.Notice
+import bot_luo_core.data.Schedule
 import bot_luo_core.data.Users
 import bot_luo_core.util.JsonWorker
 import bot_luo_core.util.Logger
 import bot_luo_core.util.Time
-import kotlinx.coroutines.TimeoutCancellationException
-import kotlinx.coroutines.cancelAndJoin
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.*
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.BotFactory
 import net.mamoe.mirai.contact.*
@@ -122,6 +121,10 @@ object BotLuo {
             CmdHandler.call(this)
             if (this.isIntercepted) Logger.log(this, Level.INFO) else Logger.log(this, Level.DEBUG)
         }
+
+        CoroutineScope(Dispatchers.IO).launch {
+            Schedule.mainClock()
+        }
     }
 
     /**

+ 1 - 0
src/main/kotlin/bot_luo_core/cli/CmdCatalog.kt

@@ -38,6 +38,7 @@ object CmdCatalog {
             PermissionCmd::class,
             PermissionGroupCmd::class,
             SayCmd::class,
+            ScheduleCmd::class,
             StatCmd::class,
             SwitchCmd::class,
             TagCmd::class

+ 3 - 3
src/main/kotlin/bot_luo_core/cli/CmdExecutable.kt

@@ -43,12 +43,12 @@ class CmdExecutable(private val cmd: KClass<out Cmd>, private val meth: KFunctio
 
     val display = cmdAnn.display
     val id get() = "$cmdName-$methName"
-    val idFixed get() = if (methName == "") cmdName else id
+    val idFixed get() = if (methName == "") cmdName else this.id
     val head get() = CMD_PREFIX[0]+idFixed
     val headPara = when {
         methAnn.alias.isEmpty() -> head
         methName == "" -> "${CMD_PREFIX[0]}$cmdName[-${methAnn.alias.joinToString(",")}]"
-        else -> "${CMD_PREFIX[0]}$id(${methAnn.alias.joinToString(",")})"
+        else -> "${CMD_PREFIX[0]}${this.id}(${methAnn.alias.joinToString(",")})"
     }
 
     val checkers = LinkedHashSet<KClass<out Checker>>().apply{
@@ -90,7 +90,7 @@ class CmdExecutable(private val cmd: KClass<out Cmd>, private val meth: KFunctio
     }
 
     override fun toString(): String {
-        return id
+        return this.id
     }
 }
 

+ 10 - 9
src/main/kotlin/bot_luo_core/cli/CmdHandler.kt

@@ -13,6 +13,7 @@ import bot_luo_core.util.Text.limitEnd
 import bot_luo_core.util.Time.notSameDayTo
 import kotlinx.coroutines.*
 import net.mamoe.mirai.event.events.GroupAwareMessageEvent
+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.content
@@ -31,18 +32,18 @@ object CmdHandler {
      *
      * 初步过滤消息,若消息以[CMD_PREFIX]开头则发起命令执行,并拦截这个消息事件
      */
-    suspend fun call(event: MessageEvent) {
-        if (event.message.content.firstNotWhitespace(3) !in CMD_PREFIX) return
+    suspend fun call(event: MessageEvent): Job? {
+        if (event.message.content.firstNotWhitespace(3) !in CMD_PREFIX) return null
         event.intercept()
         val context = CmdContext(
             MessageReader(event.message),
             Users.readUser(event.sender.id).apply { contact = event.sender } ,
-            if (event is GroupAwareMessageEvent)
+            if (event is GroupMessageEvent)
                 Groups.readGroup(event.group.id)
             else
                 Groups.virtualGroup
         )
-        call(context)
+        return call(context)
     }
 
     /**
@@ -50,10 +51,10 @@ object CmdHandler {
      *
      * @see call
      */
-    suspend fun call(event: VirtualMessageEvent) {
-        if (event.message.content.firstNotWhitespace(3) !in CMD_PREFIX) return
+    suspend fun call(event: VirtualMessageEvent): Job? {
+        if (event.message.content.firstNotWhitespace(3) !in CMD_PREFIX) return null
         event.intercept()
-        call(event.context)
+        return call(event.context)
     }
 
     /**
@@ -63,7 +64,7 @@ object CmdHandler {
      *
      * 每次call会新建一个[CoroutineScope]
      */
-    private suspend fun call(context: CmdContext) { CoroutineScope(Dispatchers.IO).launch {
+    suspend fun call(context: CmdContext): Job { CoroutineScope(Dispatchers.IO).launch {
         try {
             withAccessing(context.user, context.group, Cmds) {
                 execute(context)
@@ -126,7 +127,7 @@ object CmdHandler {
                 }
             }
         }
-    } }
+    }.let { return it } }
 
     /**
      * ## 执行命令

+ 16 - 0
src/main/kotlin/bot_luo_core/cli/commands/DebugCmd.kt

@@ -10,6 +10,7 @@ import bot_luo_core.data.Users
 import bot_luo_core.util.TableBuilder
 import bot_luo_core.util.Text.limitStart
 import bot_luo_core.util.Time.relativeTo
+import java.text.DecimalFormat
 
 @Command(
     name = "debug",
@@ -24,6 +25,7 @@ class DebugCmd(context: CmdContext) : Cmd(context) {
         val table = TableBuilder(4)
         table.th("调试信息 ——").br()
         table.tr("start_at:").td( BotLuo.startAt relativeTo context.time)
+        table.tr("mem_use:").td(formatMem(Runtime.getRuntime().totalMemory()))
         table.th()
         table.prettyLines("active_groups(${Groups.activeGroupsCount}):", Groups.groups.keys) { item, builder ->
             builder.td(item)
@@ -39,4 +41,18 @@ class DebugCmd(context: CmdContext) : Cmd(context) {
         context.print(table.toString())
         return SUCCESS
     }
+
+    companion object {
+        fun formatMem(sizeIn: Long): String {
+            val format = DecimalFormat(",###.##")
+            var size = sizeIn.toDouble()
+            if (size < 1024) return format.format(size) + "B"
+            size /= 1024
+            if (size < 1024) return format.format(size) + "KB"
+            size /= 1024
+            if (size < 1024) return format.format(size) + "MB"
+            size /= 1024
+            return format.format(size) + "GB"
+        }
+    }
 }

+ 40 - 1
src/main/kotlin/bot_luo_core/cli/commands/NoticeCmd.kt

@@ -12,6 +12,10 @@ import bot_luo_core.cli.handlers.MultiGroupArgHandler
 import bot_luo_core.cli.handlers.UserArgHandler
 import bot_luo_core.data.*
 import bot_luo_core.util.ResourceManager.downloadResource
+import bot_luo_core.util.TableBuilder
+import bot_luo_core.util.Text.limitMid
+import bot_luo_core.util.Text.miraiCodeContent
+import com.github.salomonbrys.kotson.keys
 import net.mamoe.mirai.message.data.MessageChain
 
 @Command(
@@ -25,7 +29,9 @@ import net.mamoe.mirai.message.data.MessageChain
 )
 class NoticeCmd(context: CmdContext) : Cmd(context) {
 
-    @Method(name = "", alias = ["add", "a"], pmsLevel = CmdPermissionLevel.OP, order = 0)
+    /*  ========================  add  ========================  */
+
+    @Method(name = "add-ex", alias = ["", "add", "a"], pmsLevel = CmdPermissionLevel.OP, order = 0)
     suspend fun add(
         @Argument(name = "用户", handler = UserArgHandler::class)
         user: User,
@@ -59,4 +65,37 @@ class NoticeCmd(context: CmdContext) : Cmd(context) {
         @Argument(name = "消息", handler = GreedyMessageArgHandler::class)
         msg: MessageChain
     ): CmdReceipt = add(user, arrayListOf(context.group), msg)
+
+    /*  ========================  list  ========================  */
+
+    @Method(name = "list", alias = ["l"], pmsLevel = CmdPermissionLevel.OP)
+    suspend fun list(): CmdReceipt {
+        val table = TableBuilder(4)
+        table.th("提醒列表 ——").br()
+        withLockedAccessing(Notice) {
+            for (id in Notice.element.keys()) {
+                table.tr(id).td("${Notice[id]?.size}个提醒")
+            }
+        }
+        context.print(table.toString())
+        return SUCCESS
+    }
+
+    @Method(name = "list", alias = ["l"], pmsLevel = CmdPermissionLevel.OP)
+    suspend fun list(
+        @Argument(name = "用户", handler = UserArgHandler::class)
+        user: User
+    ): CmdReceipt {
+        val table = TableBuilder(4)
+        table.th("提醒列表 —— ${user.name}(${user.id})").br()
+        withLockedAccessing(Notice) {
+            val list = Notice[user.id] ?: ArrayList()
+            for (n in list) {
+                table.tr(n.fromName.limitMid(20) + "(${n.fromId})").td(n.message.miraiCodeContent.limitMid(40))
+            }
+        }
+        context.print(table.toString())
+        return SUCCESS
+    }
+
 }

+ 247 - 0
src/main/kotlin/bot_luo_core/cli/commands/ScheduleCmd.kt

@@ -0,0 +1,247 @@
+package bot_luo_core.cli.commands
+
+import bot_luo_core.cli.Cmd
+import bot_luo_core.cli.CmdContext
+import bot_luo_core.cli.CmdPermissionLevel
+import bot_luo_core.cli.CmdReceipt
+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.handlers.GreedyMessageArgHandler
+import bot_luo_core.cli.handlers.TimeArgHandler
+import bot_luo_core.cli.handlers.TimeSpanArgHandler
+import bot_luo_core.data.Schedule
+import bot_luo_core.data.ScheduleEvent
+import bot_luo_core.data.withLockedAccessing
+import bot_luo_core.util.GSON
+import bot_luo_core.util.Logger.sendMessageWithLog
+import bot_luo_core.util.ResourceManager.deleteResourceIfNeeded
+import bot_luo_core.util.ResourceManager.downloadResource
+import bot_luo_core.util.ResourceManager.uploadResource
+import bot_luo_core.util.TableBuilder
+import bot_luo_core.util.Text.limitMid
+import bot_luo_core.util.Text.miraiCodeContent
+import bot_luo_core.util.Time
+import bot_luo_core.util.Time.relativeToE
+import com.github.salomonbrys.kotson.typeToken
+import net.mamoe.mirai.message.code.MiraiCode.deserializeMiraiCode
+import net.mamoe.mirai.message.data.MessageChain
+
+@Command(
+    name = "schedule",
+    display = "时刻表",
+    alias = ["sch"],
+    usage = "管理罗伯特时刻表",
+    notice = [
+        "添加时命令不做检查",
+        "执行时刻仅精确到分钟",
+        "周期至少为1分钟,为0则单次执行"
+    ]
+)
+class ScheduleCmd(context: CmdContext) : Cmd(context) {
+
+    /*  ========================  list  ========================  */
+
+    @Method(name = "", alias = ["list","l"], pmsLevel = CmdPermissionLevel.OP)
+    suspend fun list (): CmdReceipt {
+        val table = TableBuilder(4)
+        table.th("时刻表 ——")
+        table.tr("序号").td("时刻").td("周期").td("命令").br()
+        withLockedAccessing(Schedule) {
+            for (i in Schedule.element) {
+                val item = GSON.fromJson<ScheduleEvent>(i, typeToken<ScheduleEvent>())
+                table.tr(item.id).td(item.time relativeToE context.time).td(Time.formatSpan(item.cycle)).td(item.cmd.miraiCodeContent.limitMid(40))
+            }
+        }
+        context.print(table.toString())
+        return SUCCESS
+    }
+
+    /*  ========================  get  ========================  */
+
+    @Method(name = "get", alias = ["g"], pmsLevel = CmdPermissionLevel.OP)
+    suspend fun get(
+        @Argument(name = "序号")
+        id: Int
+    ): CmdReceipt {
+        withLockedAccessing(Schedule) {
+            val event = Schedule[id]
+            return if (event == null) {
+                context.print("不存在序号为 $id 的事件")
+                FATAL
+            } else {
+                context.withBackendBot { contact ->
+                    event.cmd = event.cmd.deserializeMiraiCode().uploadResource(contact).serializeToMiraiCode()
+                    val table = TableBuilder(4)
+                    table.th("时刻表 —— 事件($id)").br()
+                    genEventInTable(table, event)
+                    contact.sendMessageWithLog(table.toMessage())
+                }
+                SUCCESS
+            }
+        }
+    }
+
+    /*  ========================  add  ========================  */
+
+    @Method(name = "add", alias = ["a"], pmsLevel = CmdPermissionLevel.OP)
+    suspend fun add(
+        @Argument(name = "时刻", handler = TimeArgHandler::class)
+        time: Long,
+        @Argument(name = "周期", handler = TimeSpanArgHandler::class)
+        cycle: Long,
+        @Argument(name = "命令", handler = GreedyMessageArgHandler::class)
+        cmd: MessageChain
+    ): CmdReceipt {
+        withLockedAccessing(Schedule) {
+            val newId = Schedule.newId()
+            cmd.downloadResource()
+            val event = ScheduleEvent (
+                newId,
+                context.user.id,
+                context.group.id,
+                time,
+                cycle,
+                cmd.serializeToMiraiCode()
+                    )
+            Schedule.addObj(event)
+            val table = TableBuilder(4)
+            table.th("时刻表添加 —— 事件($newId)").br()
+            genEventInTable(table, event)
+            context.print(table.toMessage())
+            return SUCCESS
+        }
+    }
+
+    /*  ========================  remove  ========================  */
+
+    @Method(name = "remove", alias = ["r"], pmsLevel = CmdPermissionLevel.OP)
+    suspend fun remove(
+        @Argument(name = "序号")
+        id: Int
+    ): CmdReceipt {
+        withLockedAccessing(Schedule) {
+            val event = Schedule.removeId(id)
+            if (event == null) {
+                context.print("不存在序号为 $id 的事件")
+                return FATAL
+            }
+            val table = TableBuilder(4)
+            table.th("时刻表移除 —— 事件($id)").br()
+            context.withBackendBot { contact ->
+                event.cmd = event.cmd.deserializeMiraiCode().uploadResource(contact).serializeToMiraiCode()
+                genEventInTable(table, event)
+                contact.sendMessageWithLog(table.toMessage())
+            }
+            return SUCCESS
+        }
+    }
+
+    /*  ========================  modify  ========================  */
+
+    @Method(name = "modify", alias = ["m"], pmsLevel = CmdPermissionLevel.OP)
+    suspend fun modifyTime(
+        @Argument(name = "序号")
+        id: Int,
+        @Suppress("UNUSED_PARAMETER")
+        @Argument(name = "time", literal = true)
+        ignore: Any,
+        @Argument(name = "时刻", handler = TimeArgHandler::class)
+        time: Long
+    ): CmdReceipt {
+        withLockedAccessing(Schedule) {
+            val event = Schedule[id]
+            if (event == null) {
+                context.print("不存在序号为 $id 的事件")
+                return FATAL
+            }
+            val table = TableBuilder(4)
+            table.th("时刻表修改 —— 事件($id)[时刻]").br()
+            table.tr(Time.format(event.time)).td("->").td(Time.format(time))
+            event.time = time
+            event.user = context.user.id
+            event.group = context.group.id
+            context.withBackendBot { contact ->
+                event.cmd.deserializeMiraiCode().uploadResource(contact)
+                genEventInTable(table, event)
+                contact.sendMessageWithLog(table.toMessage())
+            }
+            Schedule.set(event)
+            return SUCCESS
+        }
+    }
+
+    @Method(name = "modify", alias = ["m"], pmsLevel = CmdPermissionLevel.OP)
+    suspend fun modifyCycle(
+        @Argument(name = "序号")
+        id: Int,
+        @Suppress("UNUSED_PARAMETER")
+        @Argument(name = "cycle", literal = true)
+        ignore: Any,
+        @Argument(name = "周期", handler = TimeSpanArgHandler::class)
+        cycle: Long
+    ): CmdReceipt {
+        withLockedAccessing(Schedule) {
+            val event = Schedule[id]
+            if (event == null) {
+                context.print("不存在序号为 $id 的事件")
+                return FATAL
+            }
+            val table = TableBuilder(4)
+            table.th("时刻表修改 —— 事件($id)[周期]").br()
+            table.tr(Time.formatSpan(event.cycle)).td("->").td(Time.formatSpan(cycle))
+            event.cycle = cycle
+            event.user = context.user.id
+            event.group = context.group.id
+            context.withBackendBot { contact ->
+                event.cmd.deserializeMiraiCode().uploadResource(contact)
+                genEventInTable(table, event)
+                contact.sendMessageWithLog(table.toMessage())
+            }
+            Schedule.set(event)
+            return SUCCESS
+        }
+    }
+
+    @Method(name = "modify", alias = ["m"], pmsLevel = CmdPermissionLevel.OP)
+    suspend fun modifyCmd(
+        @Argument(name = "序号")
+        id: Int,
+        @Suppress("UNUSED_PARAMETER")
+        @Argument(name = "cmd", literal = true)
+        ignore: Any,
+        @Argument(name = "命令", handler = GreedyMessageArgHandler::class)
+        cmd: MessageChain
+    ): CmdReceipt {
+        withLockedAccessing(Schedule) {
+            val event = Schedule[id]
+            if (event == null) {
+                context.print("不存在序号为 $id 的事件")
+                return FATAL
+            }
+            val table = TableBuilder(4)
+            table.th("时刻表修改 —— 事件($id)[时刻]").br()
+            event.user = context.user.id
+            event.group = context.group.id
+            cmd.downloadResource()
+            context.withBackendBot { contact ->
+                val cmdRaw = event.cmd.deserializeMiraiCode()
+                val cmdOld = cmdRaw.uploadResource(contact)
+                table.p(cmdOld).br().p("->").br().p(cmd)
+                event.cmd = cmd.serializeToMiraiCode()
+                genEventInTable(table, event)
+                contact.sendMessageWithLog(table.toMessage())
+                cmdRaw.deleteResourceIfNeeded()
+            }
+            return SUCCESS
+        }
+    }
+
+    fun genEventInTable(table: TableBuilder, event: ScheduleEvent) {
+        table.tr("群组:").td(event.group)
+        table.tr("用户:").td(event.user)
+        table.tr("时刻:").td(Time.format(event.time) + "  " + (event.time relativeToE context.time))
+        table.tr("周期:").td(Time.formatSpan(event.cycle))
+        table.p("命令:").sp().p(event.cmd.deserializeMiraiCode())
+    }
+}

+ 5 - 6
src/main/kotlin/bot_luo_core/cli/handlers/TimeArgHandler.kt

@@ -19,13 +19,13 @@ class TimeArgHandler: ArgHandler<Long> {
         if (reader.canRead() && reader.peek()=='~') {
             context?: throw ContextNeeded(pos, "~")
             reader.skip()
+            if (!reader.canRead()) return context.time
             val calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT+08:00"))
             val back = reader.peek() == '-'
             if (reader.peek() in "-+") reader.skip()
             var i = 0
-            do {
-                val c = reader.read()
-                when (c) {
+            while (reader.canRead() && !Character.isWhitespace(reader.peek())) {
+                when (val c = reader.read()) {
                     in '0'..'9' -> {
                         i *= 10
                         i += c - '0'
@@ -52,9 +52,8 @@ class TimeArgHandler: ArgHandler<Long> {
                     }
                     else -> throw SyntaxError(pos,"无效的时间单位 $c")
                 }
-            } while (reader.canRead() && !Character.isWhitespace(reader.peek()))
-            if (i != 0) throw SyntaxError(pos,"$i 缺少单位")
-            return calendar.timeInMillis
+            }
+            return calendar.timeInMillis + if (back) -i else i
         } else {
             val str = reader.readString()
             try {

+ 47 - 0
src/main/kotlin/bot_luo_core/cli/handlers/TimeSpanArgHandler.kt

@@ -0,0 +1,47 @@
+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.SyntaxError
+import kotlin.reflect.KType
+
+class TimeSpanArgHandler: ArgHandler<Long> {
+
+    override val name = "时间间隔参数解析器"
+
+    override fun handle(reader: MessageReader, pos: Int, argName: String?, type: KType?, context: CmdContext?): Long {
+        var i = 0L
+        var j = 0L
+        do {
+            when (val c = reader.read()) {
+                in '0'..'9' -> {
+                    i *= 10
+                    i += c - '0'
+                }
+                'd' -> {
+                    j += i*24*60*60*1000
+                    i = 0
+                }
+                'h' -> {
+                    j += i*60*60*1000
+                    i = 0
+                }
+                'm' -> if (reader.peek()=='s') {
+                    j += i
+                    i = 0
+                    reader.skip()
+                } else {
+                    j += i*60*1000
+                    i = 0
+                }
+                's' -> {
+                    j += i*1000
+                    i = 0
+                }
+                else -> throw SyntaxError(pos,"无效的时间单位 $c")
+            }
+        } while (reader.canRead() && !Character.isWhitespace(reader.peek()))
+        return j+i
+    }
+}

+ 5 - 0
src/main/kotlin/bot_luo_core/data/Config.kt

@@ -37,4 +37,9 @@ object Config {
         "UserCmdWorkingChecker",
         "BotRunningChecker"
     )
+
+    /**
+     * 主时钟周期(ms)
+     */
+    val CLOCK_CYCLE = 60_000L
 }

+ 17 - 1
src/main/kotlin/bot_luo_core/data/DataArray.kt

@@ -2,9 +2,11 @@ package bot_luo_core.data
 
 import bot_luo_core.util.GSON
 import bot_luo_core.util.clear
+import bot_luo_core.util.indices
 import com.github.salomonbrys.kotson.removeAll
 import com.google.gson.Gson
 import com.google.gson.JsonArray
+import com.google.gson.JsonElement
 import com.google.gson.JsonParser
 import com.google.gson.reflect.TypeToken
 import kotlinx.coroutines.sync.Mutex
@@ -39,7 +41,9 @@ abstract class DataArray(
 ): Data(filePath, autoSaveInterval, saveAndUnload) {
 
     /**
-     * 使用[getObj]和[setObj]进行访问
+     * 使用[getObj]和[setObj]等进行访问
+     *
+     * 若直接访问并进行了修改则需要手动调用[markDirty],否则修改不能被保存
      */
     final override val element: JsonArray
 
@@ -102,4 +106,16 @@ abstract class DataArray(
         value.forEach { v -> element.add(GSON.toJsonTree(v)) }
         changed = true
     }
+
+    fun addObj(any: Any) {
+        element.add(GSON.toJsonTree(any))
+        changed = true
+    }
+
+    fun removeObj(index: Int): JsonElement? {
+        changed = true
+        return if (index in element.indices)
+            element.remove(index)
+        else null
+    }
 }

+ 16 - 9
src/main/kotlin/bot_luo_core/data/DataObject.kt

@@ -3,10 +3,9 @@ package bot_luo_core.data
 import bot_luo_core.util.GSON
 import bot_luo_core.util.clear
 import bot_luo_core.util.set
-import com.github.salomonbrys.kotson.put
-import com.github.salomonbrys.kotson.putAll
-import com.github.salomonbrys.kotson.removeAll
+import com.github.salomonbrys.kotson.*
 import com.google.gson.Gson
+import com.google.gson.JsonElement
 import com.google.gson.JsonObject
 import com.google.gson.JsonParser
 import com.google.gson.reflect.TypeToken
@@ -42,7 +41,9 @@ abstract class DataObject(
 ): Data(filePath, autoSaveInterval, saveAndUnload) {
 
     /**
-     * 使用[getObj]和[setObj]进行访问
+     * 使用[getObj]和[setObj]等进行访问
+     *
+     * 若直接访问并进行了修改则需要手动调用[markDirty],否则修改不能被保存
      */
     final override val element: JsonObject
 
@@ -75,15 +76,13 @@ abstract class DataObject(
      *
      * 自动更新访问时间
      */
-    inline fun <reified T> getObj(key: String): T? {
+    inline fun <reified T : Any> getObj(key: String): T? {
         return if (element.has(key)) {
-            val type = object : TypeToken<T>() {}.type
-            GSON.fromJson(element[key], type)
+            GSON.fromJson(element[key], typeToken<T>())
         } else null
     }
     inline fun <reified T: Map<*, *>> getObj(): T {
-        val type = object : TypeToken<T>() {}.type
-        return GSON.fromJson(element, type)
+        return GSON.fromJson(element, typeToken<T>())
     }
 
     /**
@@ -105,4 +104,12 @@ abstract class DataObject(
         value?.forEach{ (k,v) -> element.put(k to GSON.toJsonTree(v)) }
         changed = true
     }
+
+    /**
+     * 移除一个键
+     */
+    fun removeObj(key: String): JsonElement? {
+        changed = true
+        return element.remove(key)
+    }
 }

+ 6 - 2
src/main/kotlin/bot_luo_core/data/Notice.kt

@@ -13,6 +13,7 @@ import net.mamoe.mirai.contact.Contact
 import net.mamoe.mirai.event.EventPriority
 import net.mamoe.mirai.event.GlobalEventChannel
 import net.mamoe.mirai.event.events.GroupAwareMessageEvent
+import net.mamoe.mirai.event.events.GroupMessageEvent
 import net.mamoe.mirai.event.events.MessageEvent
 import net.mamoe.mirai.message.code.MiraiCode.deserializeMiraiCode
 import net.mamoe.mirai.message.data.At
@@ -22,7 +23,7 @@ object Notice: DataObject("data/notice.json", 10000, false)
 {
     init {
         GlobalEventChannel.subscribeAlways<MessageEvent>(priority = EventPriority.HIGH) {
-            if (this is GroupAwareMessageEvent && !bot.isMainBotOf(this.group.id)) return@subscribeAlways
+            if (this is GroupMessageEvent && !bot.isMainBotOf(this.group.id)) return@subscribeAlways
             check(
                 Users.readUser(this.sender.id),
                 if (this is GroupAwareMessageEvent) Groups.readGroup(this.group.id) else Groups.virtualGroup,
@@ -38,6 +39,9 @@ object Notice: DataObject("data/notice.json", 10000, false)
         }
     }
 
+    operator fun get(id: String): ArrayList<NoticeData>? {
+        return getObj<ArrayList<NoticeData>>(id)
+    }
     operator fun get(id: Long): ArrayList<NoticeData>? {
         return getObj<ArrayList<NoticeData>>(id.toString())
     }
@@ -73,7 +77,7 @@ object Notice: DataObject("data/notice.json", 10000, false)
                 user.apply { bot = botIn }.withServeBot {
                     send(it)
                 }
-            setObj(user.id.toString(), null)
+            removeObj(user.id.toString())
         }
     }
 }

+ 83 - 0
src/main/kotlin/bot_luo_core/data/Schedule.kt

@@ -0,0 +1,83 @@
+package bot_luo_core.data
+
+import bot_luo_core.cli.CmdContext
+import bot_luo_core.cli.CmdHandler
+import bot_luo_core.cli.MessageReader
+import bot_luo_core.data.Config.CLOCK_CYCLE
+import bot_luo_core.util.GSON
+import bot_luo_core.util.Logger
+import bot_luo_core.util.ResourceManager.deleteResourceIfNeeded
+import bot_luo_core.util.Time
+import com.github.salomonbrys.kotson.typeToken
+import com.google.gson.JsonObject
+import kotlinx.coroutines.*
+import net.mamoe.mirai.message.code.MiraiCode.deserializeMiraiCode
+import org.apache.logging.log4j.Level
+
+object Schedule: DataArray("data/schedule.json", 10000, false) {
+
+    internal suspend fun mainClock() {
+        while (true) {
+            try {
+                val time = Time.time()
+                val contexts = ArrayList<CmdContext>()
+                val jobs = ArrayList<Job>()
+                withLockedAccessing(this) {
+                    val modified = ArrayList<ScheduleEvent>()
+                    for (i in element) {
+                        val event = GSON.fromJson<ScheduleEvent>(i, typeToken<ScheduleEvent>())
+                        if (event.time > time) continue
+                        contexts.add(
+                            CmdContext(
+                                MessageReader(event.cmd.deserializeMiraiCode()),
+                                Users.readUser(event.user),
+                                Groups.readGroup(event.group),
+                                event.time
+                            )
+                        )
+                        event.time += event.cycle
+                        modified.add(event)
+                        Logger.sysLog(Level.DEBUG, "<SCHEDULE> [U:(${event.user})][G:(${event.group})] -> ${event.cmd}")
+                    }
+                    for (i in modified) if (i.cycle <= 0) {
+                        removeId(i.id)?.apply {
+                            cmd.deserializeMiraiCode().deleteResourceIfNeeded()
+                        }
+                    } else set(i)
+                }
+                for (c in contexts) jobs.add(CmdHandler.call(c))
+                delay(CLOCK_CYCLE)
+                for (job in jobs) if (job.isActive) job.cancel(CancellationException("Schedule wait time out"))
+            } catch (e: Throwable) {
+                Logger.sysLog(Level.WARN, "<SCHEDULE> ${e.message}")
+                Logger.sysLog(Level.WARN, e.stackTraceToString())
+            }
+        }
+    }
+
+    operator fun get(id: Int): ScheduleEvent? {
+        return element.find { (it as JsonObject).get("id").asInt == id }?.let { GSON.fromJson(it, typeToken<ScheduleEvent>()) }
+    }
+
+    fun newId() = (1..element.size()).find { get(it) == null } ?: element.size().inc()
+
+    fun set(event: ScheduleEvent) {
+        removeId(event.id)
+        addObj(event)
+    }
+
+    fun removeId(id: Int): ScheduleEvent? {
+        val index = element.indexOfFirst { (it as JsonObject)["id"].asInt == id }
+        if (index == -1) return null
+        return removeObj(index).let { GSON.fromJson(it, typeToken<ScheduleEvent>()) }
+    }
+}
+
+data class ScheduleEvent(
+    val id: Int,
+    var user: Long,
+    var group: Long,
+    var time: Long,
+    var cycle: Long,
+    var cmd: String
+)

+ 6 - 2
src/main/kotlin/bot_luo_core/util/Logger.kt

@@ -34,14 +34,18 @@ object Logger {
     fun msgLog(level: Level, msg: String) = msgLogger.log(level, msg)
 
     fun log(e: MessageEvent, level: Level = Level.INFO) = when(e) {
-        is GroupAwareMessageEvent -> log(e, level)
+        is GroupMessageEvent -> log(e, level)
+        is GroupTempMessageEvent -> log(e, level)
         is UserMessageEvent -> log(e, level)
         else -> msgLog(level, "[U:${e.sender.nameCardOrNick}(${e.sender.id})] -> ${e.message.serializeToMiraiCode()}")
     }
 
-    fun log(e: GroupAwareMessageEvent, level: Level = Level.INFO) =
+    fun log(e: GroupMessageEvent, level: Level = Level.INFO) =
         msgLog(level, "[U:${e.sender.nameCardOrNick}(${e.sender.id})][G:${e.group.name}(${e.group.id})] -> ${e.message.serializeToMiraiCode()}")
 
+    fun log(e: GroupTempMessageEvent, level: Level = Level.INFO) =
+        msgLog(level, "[U:${e.sender.nameCardOrNick}(${e.sender.id})][B:${e.bot.nick}(${e.bot.id})] -> ${e.message.serializeToMiraiCode()}")
+
     fun log(e: UserMessageEvent, level: Level = Level.INFO) =
         msgLog(level, "[U:${e.sender.nameCardOrNick}(${e.sender.id})][B:${e.bot.nick}(${e.bot.id})] -> ${e.message.serializeToMiraiCode()}")
 

+ 1 - 1
src/main/kotlin/bot_luo_core/util/Text.kt

@@ -37,7 +37,7 @@ object Text {
     }
 
     fun String.firstNotWhitespace(max: Int = this.length): Char? {
-        for (i in 0 until max) {
+        for (i in 0 until maxOf(max, length)) {
             if (!Character.isWhitespace(this[i])) return this[i]
         }
         return null