diff --git a/detekt.yml b/detekt.yml index c15ddd5..2f0dff8 100644 --- a/detekt.yml +++ b/detekt.yml @@ -3,3 +3,6 @@ buildUponDefaultConfig: true style: WildcardImport: active: false + MaxLineLength: + active: true + maxLineLength: 150 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ca550b9..53caee9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -kotlin = "2.3.20" +kotlin = "2.4.0-Beta1" coroutines = "1.10.2" serialization = "1.10.0" ktor = "3.4.2" diff --git a/saltify-core/build.gradle.kts b/saltify-core/build.gradle.kts index 50a30e6..17eab20 100644 --- a/saltify-core/build.gradle.kts +++ b/saltify-core/build.gradle.kts @@ -39,6 +39,10 @@ kotlin { } } + compilerOptions { + freeCompilerArgs.add("-Xcontext-parameters") + } + explicitApi() } diff --git a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/annotation/ContextParametersMigrationNeeded.kt b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/annotation/ContextParametersMigrationNeeded.kt deleted file mode 100644 index 38c471d..0000000 --- a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/annotation/ContextParametersMigrationNeeded.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.ntqqrev.saltify.annotation - -/** - * 由这个注解标注的定义会在将来 Context Parameters 稳定后迁移到 extension 软件包下。 - */ -@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) -internal annotation class ContextParametersMigrationNeeded diff --git a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/builtin/plugin/CommandHelp.kt b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/builtin/plugin/CommandHelp.kt index 55bdc10..40bb2a8 100644 --- a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/builtin/plugin/CommandHelp.kt +++ b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/builtin/plugin/CommandHelp.kt @@ -4,8 +4,9 @@ import org.ntqqrev.saltify.core.forward import org.ntqqrev.saltify.core.node import org.ntqqrev.saltify.core.text import org.ntqqrev.saltify.dsl.SaltifyPlugin -import org.ntqqrev.saltify.entity.RegisteredCommandInfo -import org.ntqqrev.saltify.entity.RegisteredSubCommandInfo +import org.ntqqrev.saltify.entity.RegisteredCommand +import org.ntqqrev.saltify.entity.RegisteredSubCommand +import org.ntqqrev.saltify.extension.command import org.ntqqrev.saltify.extension.respond /** @@ -49,7 +50,7 @@ public val commandHelp: SaltifyPlugin = SaltifyPlugin("command-help") { private fun buildCommandGroupText( pluginName: String?, - commands: List + commands: List ): String = buildString { if (pluginName != null) { appendLine("插件 $pluginName: ") @@ -81,7 +82,7 @@ private fun buildCommandGroupText( }.trimEnd() private fun StringBuilder.appendSubCommand( - sub: RegisteredSubCommandInfo, + sub: RegisteredSubCommand, parentPath: String, depth: Int ) { diff --git a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/builtin/plugin/DefaultLogging.kt b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/builtin/plugin/DefaultLogging.kt index 6cfb85a..db31990 100644 --- a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/builtin/plugin/DefaultLogging.kt +++ b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/builtin/plugin/DefaultLogging.kt @@ -5,6 +5,8 @@ import kotlinx.coroutines.launch import org.ntqqrev.milky.Event import org.ntqqrev.milky.IncomingMessage import org.ntqqrev.saltify.dsl.SaltifyPlugin +import org.ntqqrev.saltify.entity.env.event +import org.ntqqrev.saltify.extension.on import org.ntqqrev.saltify.extension.plainText import org.ntqqrev.saltify.model.EventConnectionState import org.ntqqrev.saltify.model.SaltifyComponentType @@ -53,8 +55,8 @@ public val defaultLogging: SaltifyPlugin = SaltifyPlugin("default-logging" } } - // 收到消息日志 - on { event -> + // 消息日志 + on { when (val data = event.data) { is IncomingMessage.Group -> logger.debug( diff --git a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/core/SaltifyApplication.kt b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/core/SaltifyApplication.kt index 101c541..688d497 100644 --- a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/core/SaltifyApplication.kt +++ b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/core/SaltifyApplication.kt @@ -19,7 +19,7 @@ import org.ntqqrev.saltify.annotation.WithApiExtension import org.ntqqrev.saltify.dsl.SaltifyPluginContext import org.ntqqrev.saltify.dsl.config.SaltifyApplicationConfig import org.ntqqrev.saltify.entity.InstalledPlugin -import org.ntqqrev.saltify.entity.RegisteredCommandInfo +import org.ntqqrev.saltify.entity.RegisteredCommand import org.ntqqrev.saltify.exception.ApiCallException import org.ntqqrev.saltify.model.EventConnectionState import org.ntqqrev.saltify.model.EventConnectionType @@ -100,7 +100,7 @@ public sealed class SaltifyApplication(internal val config: SaltifyApplicationCo private val loadedPlugins = mutableListOf() - internal val commandRegistry: MutableList = mutableListOf() + internal val commandRegistry: MutableList = mutableListOf() @PublishedApi internal val accessToken: String? = config.connection.accessToken diff --git a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/dsl/SaltifyCommandContext.kt b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/dsl/SaltifyCommandContext.kt index 1bf17bc..ebb3448 100644 --- a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/dsl/SaltifyCommandContext.kt +++ b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/dsl/SaltifyCommandContext.kt @@ -1,43 +1,29 @@ package org.ntqqrev.saltify.dsl import io.ktor.util.logging.* -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.withTimeoutOrNull import org.ntqqrev.milky.Event -import org.ntqqrev.milky.IncomingMessage -import org.ntqqrev.milky.OutgoingSegment -import org.ntqqrev.saltify.annotation.ContextParametersMigrationNeeded import org.ntqqrev.saltify.annotation.SaltifyDsl import org.ntqqrev.saltify.core.SaltifyApplication -import org.ntqqrev.saltify.core.recallGroupMessage -import org.ntqqrev.saltify.core.recallPrivateMessage -import org.ntqqrev.saltify.entity.SaltifyCommandRequirementContext -import org.ntqqrev.saltify.extension.respond +import org.ntqqrev.saltify.entity.CommandRequirementMatch +import org.ntqqrev.saltify.entity.env.EventEnvironment import org.ntqqrev.saltify.model.CommandError import org.ntqqrev.saltify.model.CommandRequirement -import org.ntqqrev.saltify.model.milky.SendMessageOutput import kotlin.reflect.KClass -import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds @SaltifyDsl public class SaltifyCommandContext internal constructor() { + public var description: String = "" public val parameter: SaltifyParameterBuilder = SaltifyParameterBuilder(this) + @PublishedApi internal val parameters: MutableList> = mutableListOf() internal val subCommands = mutableListOf>() - internal var executionBlock: (suspend SaltifyCommandExecutionContext.() -> Unit)? = null - /** - * 指令的描述信息。 - */ - public var description: String = "" - internal var groupExecutionBlock: (suspend SaltifyCommandExecutionContext.() -> Unit)? = null - internal var privateExecutionBlock: (suspend SaltifyCommandExecutionContext.() -> Unit)? = null - internal var failureBlock: (suspend SaltifyCommandExecutionContext.(CommandError) -> Unit)? = null - internal var requirementBlock: (SaltifyCommandRequirementContext.() -> CommandRequirement)? = null + internal var executionBlock: (suspend CommandExecutionContext.() -> Unit)? = null + internal var groupExecutionBlock: (suspend CommandExecutionContext.() -> Unit)? = null + internal var privateExecutionBlock: (suspend CommandExecutionContext.() -> Unit)? = null + internal var failureBlock: (suspend CommandExecutionContext.(CommandError) -> Unit)? = null + internal var requirementBlock: (CommandRequirementMatch.() -> CommandRequirement)? = null /** * 注册一个子指令。 @@ -49,42 +35,42 @@ public class SaltifyCommandContext internal constructor() { /** * 定义指令执行要求。若不满足,静默返回。 */ - public fun require(block: SaltifyCommandRequirementContext.() -> CommandRequirement) { + public fun require(block: CommandRequirementMatch.() -> CommandRequirement) { this.requirementBlock = block } /** * 设置通用的指令执行逻辑。 */ - public fun onExecute(block: suspend SaltifyCommandExecutionContext.() -> Unit) { + public fun onExecute(block: suspend CommandExecutionContext.() -> Unit) { executionBlock = block } /** * 设置仅在群聊中触发的执行逻辑。优先级高于 [onExecute],定义后在群聊不会使用 [onExecute]。 */ - public fun onGroupExecute(block: suspend SaltifyCommandExecutionContext.() -> Unit) { + public fun onGroupExecute(block: suspend CommandExecutionContext.() -> Unit) { groupExecutionBlock = block } /** - * 设置仅在私聊中触发的执行逻辑。优先级高于 [onExecute],定义后在群聊不会使用 [onExecute]。 + * 设置仅在私聊中触发的执行逻辑。优先级高于 [onExecute],定义后在私聊不会使用 [onExecute]。 */ - public fun onPrivateExecute(block: suspend SaltifyCommandExecutionContext.() -> Unit) { + public fun onPrivateExecute(block: suspend CommandExecutionContext.() -> Unit) { privateExecutionBlock = block } /** * 当指令**解析**失败时执行的逻辑。 */ - public fun onFailure(block: suspend SaltifyCommandExecutionContext.(CommandError) -> Unit) { + public fun onFailure(block: suspend CommandExecutionContext.(CommandError) -> Unit) { failureBlock = block } } public class SaltifyParameterBuilder(@PublishedApi internal val context: SaltifyCommandContext) { /** - * 定义一个指令参数。请搭配 [SaltifyCommandExecutionContext.value] 使用。 + * 定义一个指令参数。请搭配 [CommandExecutionContext.value] 使用。 * * @param isGreedy 是否是贪婪参数,即是否包含后面的所有剩余文本。 * @param transform 将原始文本转化为目标类型的函数。 @@ -99,12 +85,12 @@ public class SaltifyParameterBuilder(@PublishedApi internal val context: Saltify } } -public class SaltifyCommandExecutionContext( - public val client: SaltifyApplication, - public val event: Event.MessageReceive, +public class CommandExecutionContext( + public override val client: SaltifyApplication, + public override val event: Event.MessageReceive, commandName: String, private val argumentMap: Map, Any?> -) { +) : EventEnvironment(event, client) { public val logger: Logger = KtorSimpleLogger("Saltify/cmd:$commandName") /** @@ -113,49 +99,6 @@ public class SaltifyCommandExecutionContext( @Suppress("UNCHECKED_CAST") public val SaltifyCommandParamDef.value: T get() = (argumentMap[this] as? ParameterParseResult.Success)?.value!! - - /** - * 响应指令。 - */ - public suspend fun respond( - block: MutableList.() -> Unit - ): SendMessageOutput = event.respond(client, block) - - /** - * 响应指令,并在指定延迟后撤回消息。 - */ - @ContextParametersMigrationNeeded - public suspend inline fun respondWithRecall( - delay: Duration, - noinline block: MutableList.() -> Unit - ) { - val output = respond(block) - delay(delay) - when (val data = event.data) { - is IncomingMessage.Group -> client.recallGroupMessage(data.peerId, output.messageSeq) - else -> client.recallPrivateMessage(data.peerId, output.messageSeq) - } - } - - /** - * 获取由指令触发者发送的下一条消息事件。超时返回 null。 - */ - public suspend fun awaitNextMessage(timeout: Duration = 30.seconds): Event.MessageReceive? { - val messageFlow = client.eventFlow.filterIsInstance() - - return withTimeoutOrNull(timeout) { - messageFlow.first { nextEvent -> - when (val contextData = event.data) { - is IncomingMessage.Group -> { - nextEvent.data is IncomingMessage.Group && - (nextEvent.data as IncomingMessage.Group).group.groupId == contextData.group.groupId && - nextEvent.senderId == event.senderId - } - else -> nextEvent.senderId == event.senderId - } - } - } - } } /** diff --git a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/dsl/SaltifyPluginContext.kt b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/dsl/SaltifyPluginContext.kt index 9bad0df..3f1f369 100644 --- a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/dsl/SaltifyPluginContext.kt +++ b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/dsl/SaltifyPluginContext.kt @@ -2,36 +2,21 @@ package org.ntqqrev.saltify.dsl import io.ktor.util.logging.* import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import org.ntqqrev.milky.Event -import org.ntqqrev.milky.IncomingMessage -import org.ntqqrev.milky.OutgoingSegment -import org.ntqqrev.saltify.annotation.ContextParametersMigrationNeeded import org.ntqqrev.saltify.annotation.SaltifyDsl import org.ntqqrev.saltify.core.SaltifyApplication -import org.ntqqrev.saltify.core.recallGroupMessage -import org.ntqqrev.saltify.core.recallPrivateMessage -import org.ntqqrev.saltify.core.text -import org.ntqqrev.saltify.entity.SaltifyBotConfig -import org.ntqqrev.saltify.extension.command -import org.ntqqrev.saltify.extension.on -import org.ntqqrev.saltify.extension.regex -import org.ntqqrev.saltify.extension.respond -import org.ntqqrev.saltify.model.milky.SendMessageOutput -import kotlin.time.Duration +import org.ntqqrev.saltify.entity.env.ApplicationEnvironment @SaltifyDsl public class SaltifyPluginContext internal constructor( pluginName: String, - public val client: SaltifyApplication, + public override val client: SaltifyApplication, @PublishedApi internal val pluginScope: CoroutineScope -) : CoroutineScope by pluginScope { +) : CoroutineScope by pluginScope, ApplicationEnvironment(client) { + public val logger: Logger = KtorSimpleLogger("Saltify/plugin:$pluginName") + internal val onStartHooks = mutableListOf Unit>() internal val onStopHooks = mutableListOf<() -> Unit>() - public val logger: Logger = KtorSimpleLogger("Saltify/plugin:$pluginName") - /** * 插件被加载,即 [SaltifyApplication.Companion.invoke] 后执行的逻辑。 */ @@ -45,64 +30,6 @@ public class SaltifyPluginContext internal constructor( public fun onStop(block: () -> Unit) { onStopHooks.add(block) } - - /** - * 注册一个事件监听器。 - */ - public inline fun on( - crossinline block: suspend SaltifyApplication.(event: T) -> Unit - ): Job = client.on(pluginScope, block) - - /** - * 注册一个消息正则匹配监听器。 - */ - public inline fun regex( - regex: String, - crossinline block: suspend SaltifyApplication.( - event: Event.MessageReceive, - matches: Sequence - ) -> Unit - ): Job = client.regex(regex, pluginScope, block) - - /** - * 注册一个指令。 - */ - public fun command( - name: String, - prefix: String = SaltifyBotConfig.commandPrefix, - block: SaltifyCommandContext.() -> Unit - ): Job = client.command(name, prefix, pluginScope, block) - - /** - * 响应事件。 - */ - public suspend fun Event.MessageReceive.respond( - block: MutableList.() -> Unit - ): SendMessageOutput = respond(client, block) - - /** - * 响应事件。这是用于返回纯文本的简写。 - */ - @ContextParametersMigrationNeeded - public suspend fun Event.MessageReceive.respond( - text: Any? - ): SendMessageOutput = respond { text(text.toString()) } - - /** - * 响应事件,并在指定延迟后撤回消息。 - */ - @ContextParametersMigrationNeeded - public suspend inline fun Event.MessageReceive.respondWithRecall( - delay: Duration, - noinline block: MutableList.() -> Unit - ) { - val output = respond(block) - delay(delay) - when (data) { - is IncomingMessage.Group -> client.recallGroupMessage(peerId, output.messageSeq) - else -> client.recallPrivateMessage(peerId, output.messageSeq) - } - } } /** diff --git a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/entity/CommandRequirementMatch.kt b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/entity/CommandRequirementMatch.kt new file mode 100644 index 0000000..497ca3e --- /dev/null +++ b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/entity/CommandRequirementMatch.kt @@ -0,0 +1,7 @@ +package org.ntqqrev.saltify.entity + +import org.ntqqrev.saltify.dsl.CommandExecutionContext + +public class CommandRequirementMatch( + public val context: CommandExecutionContext +) diff --git a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/entity/RegisteredCommandInfo.kt b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/entity/RegisteredCommand.kt similarity index 67% rename from saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/entity/RegisteredCommandInfo.kt rename to saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/entity/RegisteredCommand.kt index ac59b15..1667092 100644 --- a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/entity/RegisteredCommandInfo.kt +++ b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/entity/RegisteredCommand.kt @@ -5,21 +5,21 @@ import org.ntqqrev.saltify.dsl.SaltifyCommandParamDef /** * 子指令的注册信息 */ -public data class RegisteredSubCommandInfo( +public data class RegisteredSubCommand( val name: String, val description: String, val parameters: List>, - val subCommands: List = emptyList() + val subCommands: List = emptyList() ) /** * 已注册指令的信息 */ -public data class RegisteredCommandInfo( +public data class RegisteredCommand( val name: String, val prefix: String, val description: String, val parameters: List>, - val subCommands: List, + val subCommands: List, val pluginName: String? ) diff --git a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/entity/SaltifyCommandRequirementContext.kt b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/entity/SaltifyCommandRequirementContext.kt deleted file mode 100644 index 0bbfedb..0000000 --- a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/entity/SaltifyCommandRequirementContext.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.ntqqrev.saltify.entity - -import org.ntqqrev.saltify.dsl.SaltifyCommandExecutionContext - -public class SaltifyCommandRequirementContext( - public val context: SaltifyCommandExecutionContext -) diff --git a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/entity/env/ApplicationEnvironment.kt b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/entity/env/ApplicationEnvironment.kt new file mode 100644 index 0000000..19a438b --- /dev/null +++ b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/entity/env/ApplicationEnvironment.kt @@ -0,0 +1,10 @@ +package org.ntqqrev.saltify.entity.env + +import org.ntqqrev.saltify.core.SaltifyApplication + +public open class ApplicationEnvironment( + public open val client: SaltifyApplication +) + +context(ctx: ApplicationEnvironment) +public val client: SaltifyApplication get() = ctx.client diff --git a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/entity/env/EventEnvironment.kt b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/entity/env/EventEnvironment.kt new file mode 100644 index 0000000..bdf7068 --- /dev/null +++ b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/entity/env/EventEnvironment.kt @@ -0,0 +1,15 @@ +package org.ntqqrev.saltify.entity.env + +import org.ntqqrev.milky.Event +import org.ntqqrev.saltify.core.SaltifyApplication + +public open class EventEnvironment( + public open val event: T, + public override val client: SaltifyApplication +) : ApplicationEnvironment(client) + +context(ctx: EventEnvironment) +public val event: T get() = ctx.event + +context(ctx: EventEnvironment) +public val client: SaltifyApplication get() = ctx.client diff --git a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/extension/ApplicationExtension.kt b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/extension/ApplicationExtension.kt index d95d093..11c7364 100644 --- a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/extension/ApplicationExtension.kt +++ b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/extension/ApplicationExtension.kt @@ -10,12 +10,16 @@ import org.ntqqrev.milky.IncomingSegment import org.ntqqrev.saltify.core.SaltifyApplication import org.ntqqrev.saltify.dsl.ParameterParseResult import org.ntqqrev.saltify.dsl.SaltifyCommandContext -import org.ntqqrev.saltify.dsl.SaltifyCommandExecutionContext +import org.ntqqrev.saltify.dsl.CommandExecutionContext import org.ntqqrev.saltify.dsl.SaltifyCommandParamDef -import org.ntqqrev.saltify.entity.RegisteredCommandInfo -import org.ntqqrev.saltify.entity.RegisteredSubCommandInfo +import org.ntqqrev.saltify.entity.RegisteredCommand +import org.ntqqrev.saltify.entity.RegisteredSubCommand import org.ntqqrev.saltify.entity.SaltifyBotConfig -import org.ntqqrev.saltify.entity.SaltifyCommandRequirementContext +import org.ntqqrev.saltify.entity.CommandRequirementMatch +import org.ntqqrev.saltify.entity.env.ApplicationEnvironment +import org.ntqqrev.saltify.entity.env.EventEnvironment +import org.ntqqrev.saltify.entity.env.client +import org.ntqqrev.saltify.entity.env.event import org.ntqqrev.saltify.model.CommandError import org.ntqqrev.saltify.model.SaltifyComponentType import org.ntqqrev.saltify.util.coroutine.runCatchingToExceptionFlow @@ -25,15 +29,18 @@ import kotlin.time.Clock /** * 注册一个事件监听器。 */ -public inline fun SaltifyApplication.on( - scope: CoroutineScope = extensionScope, - crossinline block: suspend SaltifyApplication.(event: T) -> Unit +context(_: ApplicationEnvironment) +public inline fun on( + scope: CoroutineScope = client.extensionScope, + crossinline block: suspend context(EventEnvironment) () -> Unit ): Job = scope.launch { - eventFlow + client.eventFlow .filterIsInstance() .collect { launch { - runCatchingToExceptionFlow { block(it) } + runCatchingToExceptionFlow { + context(EventEnvironment(it, client)) { block() } + } } } } @@ -41,19 +48,20 @@ public inline fun SaltifyApplication.on( /** * 注册一个消息正则匹配监听器。 */ -public inline fun SaltifyApplication.regex( +context(_: ApplicationEnvironment) +public inline fun regex( regex: String, - scope: CoroutineScope = extensionScope, - crossinline block: suspend SaltifyApplication.(event: Event.MessageReceive, matches: Sequence) -> Unit + scope: CoroutineScope = client.extensionScope, + crossinline block: suspend context(EventEnvironment) (matches: Sequence) -> Unit ): Job { val regex = Regex(regex) - return on(scope) { event -> + return on(scope) { val text = event.segments.filterIsInstance() .joinToString("") { it.text } val matches = regex.findAll(text) - if (matches.any()) block(event, matches) + if (matches.any()) block(matches) } } @@ -62,30 +70,31 @@ private val spaceRegex = Regex("\\s+") /** * 注册一个指令。 */ -public fun SaltifyApplication.command( +context(_: ApplicationEnvironment) +public fun command( name: String, prefix: String = SaltifyBotConfig.commandPrefix, - scope: CoroutineScope = extensionScope, + scope: CoroutineScope = client.extensionScope, builder: SaltifyCommandContext.() -> Unit ): Job { val rootDsl = SaltifyCommandContext().apply(builder) val component = scope.coroutineContext.saltifyComponent val pluginName = if (component?.type == SaltifyComponentType.Plugin) component.name else null - commandRegistry.add( - RegisteredCommandInfo( + client.commandRegistry.add( + RegisteredCommand( name = name, prefix = prefix, description = rootDsl.description, parameters = rootDsl.parameters.toList(), subCommands = rootDsl.subCommands.map { (subName, subCtx) -> - subCtx.toSubCommandInfo(subName) + subCtx.toSubCommand(subName) }, pluginName = pluginName ) ) - return on(scope) { event -> + return on(scope) { val rawText = event.segments.filterIsInstance() .joinToString("") { it.text } .trim() @@ -93,7 +102,7 @@ public fun SaltifyApplication.command( val tokens = if (rawText.isEmpty()) emptyList() else rawText.split(spaceRegex) if (tokens.isEmpty() || tokens[0] != "$prefix$name") return@on - executeCommand(rootDsl, tokens.drop(1), this, event, name) + executeCommand(rootDsl, tokens.drop(1), client, event, name) } } @@ -105,10 +114,10 @@ private suspend fun executeCommand( name: String ) { val argumentMap = mutableMapOf, ParameterParseResult>() - val execution = SaltifyCommandExecutionContext(client, event, name, argumentMap) + val execution = CommandExecutionContext(client, event, name, argumentMap) dsl.requirementBlock?.let { block -> - val requirement = SaltifyCommandRequirementContext(execution).block() + val requirement = CommandRequirementMatch(execution).block() if (!requirement.satisfies()) return } @@ -162,10 +171,10 @@ private suspend fun executeCommand( execution.logger.info("seq=${event.messageSeq} 处理完成, 用时 ${Clock.System.now() - startInstant}") } -private fun SaltifyCommandContext.toSubCommandInfo(name: String): RegisteredSubCommandInfo = - RegisteredSubCommandInfo( +private fun SaltifyCommandContext.toSubCommand(name: String): RegisteredSubCommand = + RegisteredSubCommand( name = name, description = description, parameters = parameters.toList(), - subCommands = subCommands.map { (subName, subCtx) -> subCtx.toSubCommandInfo(subName) } + subCommands = subCommands.map { (subName, subCtx) -> subCtx.toSubCommand(subName) } ) diff --git a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/extension/CommandContextExtension.kt b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/extension/CommandContextExtension.kt index 754a8fe..5ccfc37 100644 --- a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/extension/CommandContextExtension.kt +++ b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/extension/CommandContextExtension.kt @@ -1,50 +1,41 @@ package org.ntqqrev.saltify.extension -import org.ntqqrev.saltify.core.text -import org.ntqqrev.saltify.dsl.SaltifyCommandExecutionContext +import org.ntqqrev.saltify.dsl.CommandExecutionContext import org.ntqqrev.saltify.dsl.SaltifyCommandParamDef import org.ntqqrev.saltify.dsl.SaltifyParameterBuilder -import org.ntqqrev.saltify.model.milky.SendMessageOutput /** - * 定义一个指令参数。请搭配 [SaltifyCommandExecutionContext.value] 使用。 + * 定义一个指令参数。请搭配 [CommandExecutionContext.value] 使用。 */ public fun SaltifyParameterBuilder.string(name: String, desc: String = ""): SaltifyCommandParamDef = from(name, desc) { it } /** - * 定义一个指令参数。请搭配 [SaltifyCommandExecutionContext.value] 使用。 + * 定义一个指令参数。请搭配 [CommandExecutionContext.value] 使用。 */ public fun SaltifyParameterBuilder.int(name: String, desc: String = ""): SaltifyCommandParamDef = from(name, desc) { it.toIntOrNull() } /** - * 定义一个指令参数。请搭配 [SaltifyCommandExecutionContext.value] 使用。 + * 定义一个指令参数。请搭配 [CommandExecutionContext.value] 使用。 */ public fun SaltifyParameterBuilder.long(name: String, desc: String = ""): SaltifyCommandParamDef = from(name, desc) { it.toLongOrNull() } /** - * 定义一个指令参数。请搭配 [SaltifyCommandExecutionContext.value] 使用。 + * 定义一个指令参数。请搭配 [CommandExecutionContext.value] 使用。 */ public fun SaltifyParameterBuilder.boolean(name: String, desc: String = ""): SaltifyCommandParamDef = from(name, desc) { it.toBooleanStrictOrNull() } /** - * 定义一个指令参数。请搭配 [SaltifyCommandExecutionContext.value] 使用。 + * 定义一个指令参数。请搭配 [CommandExecutionContext.value] 使用。 */ public fun SaltifyParameterBuilder.double(name: String, desc: String = ""): SaltifyCommandParamDef = from(name, desc) { it.toDoubleOrNull() } /** - * 定义一个贪婪字符串参数。该参数会捕获剩余的**所有**文本内容。请搭配 [SaltifyCommandExecutionContext.value] 使用。 + * 定义一个贪婪字符串参数。该参数会捕获剩余的**所有**文本内容。请搭配 [CommandExecutionContext.value] 使用。 */ public fun SaltifyParameterBuilder.greedyString(name: String, desc: String = ""): SaltifyCommandParamDef = from(name, desc, isGreedy = true) { it } - -/** - * 响应事件。这是用于返回纯文本的简写。 - */ -public suspend inline fun SaltifyCommandExecutionContext.respond( - text: Any? -): SendMessageOutput = respond { text(text.toString()) } diff --git a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/extension/EventExtension.kt b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/extension/EventExtension.kt index c2831c8..f131f6d 100644 --- a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/extension/EventExtension.kt +++ b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/extension/EventExtension.kt @@ -1,36 +1,89 @@ package org.ntqqrev.saltify.extension +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withTimeoutOrNull import org.ntqqrev.milky.Event import org.ntqqrev.milky.IncomingMessage import org.ntqqrev.milky.OutgoingSegment -import org.ntqqrev.saltify.core.SaltifyApplication +import org.ntqqrev.saltify.core.recallGroupMessage +import org.ntqqrev.saltify.core.recallPrivateMessage import org.ntqqrev.saltify.core.sendGroupMessage import org.ntqqrev.saltify.core.sendPrivateMessage import org.ntqqrev.saltify.core.text -import org.ntqqrev.saltify.dsl.SaltifyPluginContext +import org.ntqqrev.saltify.entity.env.EventEnvironment import org.ntqqrev.saltify.model.milky.SendMessageOutput +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds /** - * 响应事件。鉴于 Context Parameter 尚未完善,这里需要手动传 client。建议使用 [SaltifyPluginContext.respond]。 + * 响应事件。 */ -public suspend fun Event.MessageReceive.respond( - client: SaltifyApplication, +context(ctx: EventEnvironment) +public suspend fun respond( block: MutableList.() -> Unit -): SendMessageOutput = when (data) { +): SendMessageOutput = when (ctx.event.data) { is IncomingMessage.Group -> { - val output = client.sendGroupMessage(peerId, block) + val output = ctx.client.sendGroupMessage(ctx.event.peerId, block) SendMessageOutput(output.messageSeq, output.time) } else -> { - val output = client.sendPrivateMessage(peerId, block) + val output = ctx.client.sendPrivateMessage(ctx.event.peerId, block) SendMessageOutput(output.messageSeq, output.time) } } /** - * 响应事件。这是用于返回纯文本的简写。鉴于 Context Parameter 尚未完善,这里需要手动传 client。 + * 响应事件。这是用于返回纯文本的简写形式。 */ -public suspend inline fun Event.MessageReceive.respond( - client: SaltifyApplication, +context(_: EventEnvironment) +public suspend inline fun respond( text: Any? -): SendMessageOutput = respond(client) { text(text.toString()) } +): SendMessageOutput = respond { text(text.toString()) } + +/** + * 响应事件,并在指定延迟后撤回消息。 + */ +context(ctx: EventEnvironment) +public suspend inline fun respondWithRecall( + delay: Duration, + noinline block: MutableList.() -> Unit +) { + val output = respond(block) + delay(delay) + when (val data = ctx.event.data) { + is IncomingMessage.Group -> ctx.client.recallGroupMessage(data.peerId, output.messageSeq) + else -> ctx.client.recallPrivateMessage(data.peerId, output.messageSeq) + } +} + +/** + * 响应事件,并在指定延迟后撤回消息。这是用于返回纯文本的简写形式。 + */ +context(_: EventEnvironment) +public suspend inline fun respondWithRecall( + delay: Duration, + text: Any? +): Unit = respondWithRecall(delay) { text(text.toString()) } + +/** + * 获取由事件触发者发送的下一条消息事件。超时返回 null。 + */ +context(ctx: EventEnvironment) +public suspend fun awaitNextMessage(timeout: Duration = 30.seconds): Event.MessageReceive? { + val messageFlow = ctx.client.eventFlow.filterIsInstance() + + return withTimeoutOrNull(timeout) { + messageFlow.first { nextEvent -> + when (val contextData = ctx.event.data) { + is IncomingMessage.Group -> { + nextEvent.data is IncomingMessage.Group && + (nextEvent.data as IncomingMessage.Group).group.groupId == contextData.group.groupId && + nextEvent.senderId == ctx.event.senderId + } + else -> nextEvent.senderId == ctx.event.senderId + } + } + } +} diff --git a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/extension/PluginContextExtension.kt b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/extension/PluginContextExtension.kt deleted file mode 100644 index 1ef17a9..0000000 --- a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/extension/PluginContextExtension.kt +++ /dev/null @@ -1 +0,0 @@ -package org.ntqqrev.saltify.extension diff --git a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/extension/RequirementExtension.kt b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/extension/RequirementExtension.kt index 28d250d..9af12ba 100644 --- a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/extension/RequirementExtension.kt +++ b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/extension/RequirementExtension.kt @@ -2,41 +2,41 @@ package org.ntqqrev.saltify.extension import org.ntqqrev.milky.IncomingMessage import org.ntqqrev.milky.IncomingSegment -import org.ntqqrev.saltify.entity.SaltifyCommandRequirementContext +import org.ntqqrev.saltify.entity.CommandRequirementMatch import org.ntqqrev.saltify.model.CommandRequirement import org.ntqqrev.saltify.model.PermissionLevel -public fun SaltifyCommandRequirementContext.user(vararg targetId: Long): CommandRequirement = +public fun CommandRequirementMatch.user(vararg targetId: Long): CommandRequirement = CommandRequirement { context.event.senderId in targetId } -public fun SaltifyCommandRequirementContext.group(vararg targetId: Long): CommandRequirement = +public fun CommandRequirementMatch.group(vararg targetId: Long): CommandRequirement = CommandRequirement { ((context.event.data as? IncomingMessage.Group)?.group?.groupId ?: return@CommandRequirement false) in targetId } -public fun SaltifyCommandRequirementContext.perm(targetLevel: PermissionLevel): CommandRequirement = +public fun CommandRequirementMatch.perm(targetLevel: PermissionLevel): CommandRequirement = CommandRequirement { permissionLevelOf(context.event.senderId) >= targetLevel } -public val SaltifyCommandRequirementContext.isGroupAdmin: CommandRequirement +public val CommandRequirementMatch.isGroupAdmin: CommandRequirement get() = CommandRequirement { val data = context.event.data as? IncomingMessage.Group data?.groupMember?.role == "admin" } -public val SaltifyCommandRequirementContext.isGroupOwner: CommandRequirement +public val CommandRequirementMatch.isGroupOwner: CommandRequirement get() = CommandRequirement { val data = context.event.data as? IncomingMessage.Group data?.groupMember?.role == "owner" } -public val SaltifyCommandRequirementContext.isGroupAdminOrOwner: CommandRequirement +public val CommandRequirementMatch.isGroupAdminOrOwner: CommandRequirement get() = isGroupAdmin or isGroupOwner -public val SaltifyCommandRequirementContext.isMention: CommandRequirement +public val CommandRequirementMatch.isMention: CommandRequirement get() = CommandRequirement { val segment = context.event.segments.filterIsInstance() segment.isNotEmpty() && segment.any { it.userId == context.event.senderId } diff --git a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/util/coroutine/RunCatchingToExceptionFlow.kt b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/util/coroutine/RunCatchingToExceptionFlow.kt index 3ea5020..2c80f7c 100644 --- a/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/util/coroutine/RunCatchingToExceptionFlow.kt +++ b/saltify-core/src/commonMain/kotlin/org/ntqqrev/saltify/util/coroutine/RunCatchingToExceptionFlow.kt @@ -2,13 +2,15 @@ package org.ntqqrev.saltify.util.coroutine import kotlinx.coroutines.CancellationException import kotlinx.coroutines.currentCoroutineContext -import org.ntqqrev.saltify.core.SaltifyApplication +import org.ntqqrev.saltify.entity.env.ApplicationEnvironment +import org.ntqqrev.saltify.entity.env.client import kotlin.coroutines.CoroutineContext /** * 执行代码块,并将可能的异常发送到全局异常流。 */ -public suspend inline fun SaltifyApplication.runCatchingToExceptionFlow( +context(_: ApplicationEnvironment) +public suspend inline fun runCatchingToExceptionFlow( context: CoroutineContext? = null, crossinline block: suspend () -> Unit ) { @@ -16,6 +18,6 @@ public suspend inline fun SaltifyApplication.runCatchingToExceptionFlow( block() }.onFailure { throwable -> if (throwable is CancellationException) throw throwable - exceptionHandlerProvider.exceptionFlow.tryEmit((context ?: currentCoroutineContext()) to throwable) + client.exceptionHandlerProvider.exceptionFlow.tryEmit((context ?: currentCoroutineContext()) to throwable) } } diff --git a/saltify-core/src/jvmTest/kotlin/org/ntqqrev/saltify/PluginTest.kt b/saltify-core/src/jvmTest/kotlin/org/ntqqrev/saltify/PluginTest.kt index 0caaa46..74e7b36 100644 --- a/saltify-core/src/jvmTest/kotlin/org/ntqqrev/saltify/PluginTest.kt +++ b/saltify-core/src/jvmTest/kotlin/org/ntqqrev/saltify/PluginTest.kt @@ -8,9 +8,12 @@ import org.ntqqrev.saltify.builtin.plugin.defaultLogging import org.ntqqrev.saltify.core.SaltifyApplication import org.ntqqrev.saltify.core.getLoginInfo import org.ntqqrev.saltify.dsl.SaltifyPlugin +import org.ntqqrev.saltify.extension.awaitNextMessage +import org.ntqqrev.saltify.extension.command import org.ntqqrev.saltify.extension.greedyString import org.ntqqrev.saltify.extension.int import org.ntqqrev.saltify.extension.plainText +import org.ntqqrev.saltify.extension.regex import org.ntqqrev.saltify.extension.respond import org.ntqqrev.saltify.model.EventConnectionType import kotlin.test.Test @@ -61,8 +64,8 @@ class PluginTest { } // regex test - regex("""BV1\w{9}""") { event, matches -> - event.respond(matches.joinToString { it.value }) + regex("""BV1\w{9}""") { matches -> + respond(matches.joinToString { it.value }) } // config test diff --git a/saltify-docs/content/docs-core/command.md b/saltify-docs/content/docs-core/command.md index ec5b4bf..0daf113 100644 --- a/saltify-docs/content/docs-core/command.md +++ b/saltify-docs/content/docs-core/command.md @@ -8,6 +8,8 @@ Saltify 提供了一套类型安全的指令构建 DSL,支持参数解析、 ```kotlin client.command("order", prefix = "/") { + description = "创建一个订单" + // 定义一个 Int 类型的参数 val id = parameter.int("id") // 定义一个贪婪字符串参数,即将之后所有内容视为一个参数