Merge remote-tracking branch 'origin/master' into dependencies

# Conflicts:
#	backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/BuiltInJvmPluginLoaderImpl.kt
#	backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/JvmPluginClassLoader.kt
This commit is contained in:
Karlatemp 2020-10-10 11:42:24 +08:00
commit bf40b6036b
No known key found for this signature in database
GPG Key ID: 21FBDDF664FF06F8
148 changed files with 4980 additions and 1166 deletions

5
.editorconfig Normal file
View File

@ -0,0 +1,5 @@
[*.{kt, kts}]
max_line_length = 160
tab_width = 4
ij_continuation_indent_size = 4
indent_size = 4

63
.github/workflows/Publishing.yml vendored Normal file
View File

@ -0,0 +1,63 @@
# This is a basic workflow to help you get started with Actions
name: Bintray Publish
# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
release:
types:
- created
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Gradle clean
run: ./gradlew clean
- name: Gradle build
run: ./gradlew build # if test's failed, don't publish
- name: Check keys
run: ./gradlew
:mirai-console:ensureBintrayAvailable
-Dbintray_user=${{ secrets.BINTRAY_USER }} -Pbintray_user=${{ secrets.BINTRAY_USER }}
-Dbintray_key=${{ secrets.BINTRAY_KEY }} -Pbintray_key=${{ secrets.BINTRAY_KEY }}
- name: Gradle :mirai-console:fillBuildConstants
run: ./gradlew
:mirai-console:fillBuildConstants
-Dbintray_user=${{ secrets.BINTRAY_USER }} -Pbintray_user=${{ secrets.BINTRAY_USER }}
-Dbintray_key=${{ secrets.BINTRAY_KEY }} -Pbintray_key=${{ secrets.BINTRAY_KEY }}
- name: Gradle :mirai-console:bintrayUpload
run: ./gradlew
:mirai-console:bintrayUpload
-Dbintray_user=${{ secrets.BINTRAY_USER }} -Pbintray_user=${{ secrets.BINTRAY_USER }}
-Dbintray_key=${{ secrets.BINTRAY_KEY }} -Pbintray_key=${{ secrets.BINTRAY_KEY }}
- name: Gradle :mirai-console-terminal:bintrayUpload
run: ./gradlew
:mirai-console-terminal:bintrayUpload
-Dbintray_user=${{ secrets.BINTRAY_USER }} -Pbintray_user=${{ secrets.BINTRAY_USER }}
-Dbintray_key=${{ secrets.BINTRAY_KEY }} -Pbintray_key=${{ secrets.BINTRAY_KEY }}
- name: Gradle :mirai-console-compiler-common:bintrayUpload
run: ./gradlew
:mirai-console-compiler-common:bintrayUpload
-Dbintray_user=${{ secrets.BINTRAY_USER }} -Pbintray_user=${{ secrets.BINTRAY_USER }}
-Dbintray_key=${{ secrets.BINTRAY_KEY }} -Pbintray_key=${{ secrets.BINTRAY_KEY }}
- name: Gradle :mirai-console-intellij:bintrayUpload
run: ./gradlew
:mirai-console-intellij:bintrayUpload
-Dbintray_user=${{ secrets.BINTRAY_USER }} -Pbintray_user=${{ secrets.BINTRAY_USER }}
-Dbintray_key=${{ secrets.BINTRAY_KEY }} -Pbintray_key=${{ secrets.BINTRAY_KEY }}
- name: Publish Gradle plugin
run: ./gradlew
:mirai-console-gradle:publishPlugins
-Dgradle.publish.key=${{ secrets.GRADLE_PUBLISH_KEY }} -Pgradle.publish.key=${{ secrets.GRADLE_PUBLISH_KEY }}
-Dgradle.publish.secret=${{ secrets.GRADLE_PUBLISH_SECRET }} -Pgradle.publish.secret=${{ secrets.GRADLE_PUBLISH_SECRET }}

View File

@ -1,37 +0,0 @@
# This is a basic workflow to help you get started with Actions
name: Bintray Publish
# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
release:
types:
- created
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Gradle clean
run: ./gradlew clean
- name: Gradle build
run: ./gradlew build # if test's failed, don't publish
- name: Check keys
run: ./gradlew :mirai-console:ensureBintrayAvailable -Dbintray_user=${{ secrets.BINTRAY_USER }} -Pbintray_user=${{ secrets.BINTRAY_USER }} -Dbintray_key=${{ secrets.BINTRAY_KEY }} -Pbintray_key=${{ secrets.BINTRAY_KEY }}
- name: Gradle :mirai-console:fillBuildConstants
run: ./gradlew :mirai-console:fillBuildConstants -Dbintray_user=${{ secrets.BINTRAY_USER }} -Pbintray_user=${{ secrets.BINTRAY_USER }} -Dbintray_key=${{ secrets.BINTRAY_KEY }} -Pbintray_key=${{ secrets.BINTRAY_KEY }}
- name: Gradle :mirai-console:bintrayUpload
run: ./gradlew :mirai-console:bintrayUpload -Dbintray_user=${{ secrets.BINTRAY_USER }} -Pbintray_user=${{ secrets.BINTRAY_USER }} -Dbintray_key=${{ secrets.BINTRAY_KEY }} -Pbintray_key=${{ secrets.BINTRAY_KEY }}
- name: Gradle :mirai-console-pure:bintrayUpload
run: ./gradlew :mirai-console-pure:bintrayUpload -Dbintray_user=${{ secrets.BINTRAY_USER }} -Pbintray_user=${{ secrets.BINTRAY_USER }} -Dbintray_key=${{ secrets.BINTRAY_KEY }} -Pbintray_key=${{ secrets.BINTRAY_KEY }}

View File

@ -28,8 +28,8 @@ jobs:
run: ./gradlew build # if test's failed, don't publish run: ./gradlew build # if test's failed, don't publish
- name: Gradle :mirai-console:githubUpload - name: Gradle :mirai-console:githubUpload
run: ./gradlew :mirai-console:githubUpload -Dgithub_token=${{ secrets.MAMOE_TOKEN }} -Pgithub_token=${{ secrets.MAMOE_TOKEN }} run: ./gradlew :mirai-console:githubUpload -Dgithub_token=${{ secrets.MAMOE_TOKEN }} -Pgithub_token=${{ secrets.MAMOE_TOKEN }}
- name: Gradle :mirai-console-pure:githubUpload - name: Gradle :mirai-console-terminal:githubUpload
run: ./gradlew :mirai-console-pure:githubUpload -Dgithub_token=${{ secrets.MAMOE_TOKEN }} -Pgithub_token=${{ secrets.MAMOE_TOKEN }} run: ./gradlew :mirai-console-terminal:githubUpload -Dgithub_token=${{ secrets.MAMOE_TOKEN }} -Pgithub_token=${{ secrets.MAMOE_TOKEN }}
# - name: Upload artifact # - name: Upload artifact

BIN
.idea/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -14,51 +14,33 @@ Mirai 是一个在全平台下运行,提供 QQ 协议支持的高效率机器
</div> </div>
# mirai-console # mirai-console
[ ![Download](https://api.bintray.com/packages/him188moe/mirai/mirai-console/images/download.svg?) ](https://bintray.com/him188moe/mirai/mirai-console/)
高效率插件支持 QQ 机器人框架, 机器人核心来自 [mirai](https://github.com/mamoe/mirai) 高效率 QQ 机器人框架,机器人核心来自 [mirai](https://github.com/mamoe/mirai)
## 模块说明 ![Gradle CI](https://github.com/mamoe/mirai-console/workflows/Gradle%20CI/badge.svg?branch=master)
[![Gitter](https://badges.gitter.im/mamoe/mirai.svg)](https://gitter.im/mamoe/mirai?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
console 由后端和前端一起工作. 使用时必须选择一个前端. ## 开发
- `mirai-console`: console 的后端, 包含插件管理, 指令系统, 配置系统. - **[准备工作 - 环境和前置知识](docs/Preparations.md)**
- **[配置项目](docs/ConfiguringProjects.md)**
- **[启动 Console](docs/Run.md)**
### 后端插件开发基础
- 插件 - [Plugin 模块](docs/Plugins.md)
- 指令 - [Command 模块](docs/Commands.md)
- 存储 - [PluginData 模块](docs/PluginData.md)
- 权限 - [Permission 模块](docs/Permissions.md)
**示例插件**
- [mirai-console-example-plugin (Kotlin DSL)](https://github.com/Him188/mirai-console-example-plugin)
- [mirai-console-example-plugin (Groovy DSL)](https://github.com/Karlatemp/mirai-console-example-plugin)
前端: ### 后端插件开发进阶
- `mirai-console-pure`: console 的轻量命令行前端. - 扩展 - [Extension 模块和扩展点](docs/Extensions.md)
- `mirai-console-graphical`: console 的 JavaFX 图形化界面前端. (开发中)
- `mirai-console-terminal`: console 的 Unix 终端界面前端. (开发中)
### 实现前端
**注意:`mirai-console` 后端和 pure 前端正在进行完全的重构, 所有 API 都不具有稳定性** - [FrontEnd](docs/FrontEnd.md)
### 使用
**查看示例插件**: [mirai-console-example-plugin](https://github.com/Him188/mirai-console-example-plugin)
正在更新中的文档:[参考文档](docs/README.md)
#### Gradle
`CORE_VERSION`: [ ![Download](https://api.bintray.com/packages/him188moe/mirai/mirai-core/images/download.svg?) ](https://bintray.com/him188moe/mirai/mirai-core/)
`CONSOLE_VERSION`: [ ![Download](https://api.bintray.com/packages/him188moe/mirai/mirai-console/images/download.svg?) ](https://bintray.com/him188moe/mirai/mirai-console/)
build.gradle.kts
```kotlin
repositories {
jcenter()
}
dependencies {
implementation("net.mamoe:mirai-core:$CORE_VERSION") // mirai-core 的 API
implementation("net.mamoe:mirai-console:$CONSOLE_VERSION") // 后端
testImplementation("net.mamoe:mirai-console-pure:$CONSOLE_VERSION") // 前端, 用于启动测试
}
```
#### Maven
同理 Gradle

View File

@ -0,0 +1,6 @@
# Mirai Console - Backend.codegen
后端代码生成模块,用于最小化重复代码的人工成本。
- `MessageScope` 代码生成: [MessageScopeCodegen.kt: Line 33](src/main/kotlin/net/mamoe/mirai/console/codegen/MessageScopeCodegen.kt#L33)
- `Value``PluginData` 相关代码生成: [ValueSettingCodegen.kt: Line 18](src/main/kotlin/net/mamoe/mirai/console/codegen/ValuePluginDataCodegen.kt#L18)

View File

@ -0,0 +1,3 @@
# Mirai Console - Backend
Mirai Console 后端模块. 发布为 `net.mamoe:mirai-console`.

View File

@ -56,50 +56,26 @@ kotlin {
} }
dependencies { dependencies {
implementation("net.mamoe:mirai-core:${Versions.core}") compileAndTestRuntime("net.mamoe:mirai-core:${Versions.core}")
compileAndTestRuntime(kotlin("stdlib", Versions.kotlinStdlib))
compileAndTestRuntime(kotlin("stdlib-jdk8", Versions.kotlinStdlib))
implementation(kotlinx("serialization-core", Versions.serialization)) compileAndTestRuntime("org.jetbrains.kotlinx:atomicfu:${Versions.atomicFU}")
implementation(kotlin("reflect")) compileAndTestRuntime(kotlinx("coroutines-core", Versions.coroutines))
compileAndTestRuntime(kotlinx("serialization-core", Versions.serialization))
compileAndTestRuntime(kotlin("reflect"))
api("net.mamoe.yamlkt:yamlkt:${Versions.yamlkt}") smartImplementation("net.mamoe.yamlkt:yamlkt:${Versions.yamlkt}")
implementation("org.jetbrains.kotlinx:atomicfu:${Versions.atomicFU}") smartImplementation("org.jetbrains:annotations:19.0.0")
api("org.jetbrains:annotations:19.0.0") smartApi(kotlinx("coroutines-jdk8", Versions.coroutines))
api(kotlinx("coroutines-jdk8", Versions.coroutines))
api("com.vdurmont:semver4j:3.1.0")
//api(kotlinx("collections-immutable", Versions.collectionsImmutable))
testApi(kotlinx("serialization-core", Versions.serialization))
testApi("net.mamoe:mirai-core-qqandroid:${Versions.core}") testApi("net.mamoe:mirai-core-qqandroid:${Versions.core}")
testApi(kotlin("stdlib-jdk8")) testApi(kotlin("stdlib-jdk8"))
testApi(kotlin("test")) testApi(kotlin("test"))
testApi(kotlin("test-junit5")) testApi(kotlin("test-junit5"))
testImplementation("org.junit.jupiter:junit-jupiter-api:5.2.0") testApi("org.junit.jupiter:junit-jupiter-api:5.2.0")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.2.0") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.2.0")
// val autoService = "1.0-rc7"
// kapt("com.google.auto.service", "auto-service", autoService)
// compileOnly("com.google.auto.service", "auto-service-annotations", autoService)
}
ext.apply {
// 傻逼 compileAndRuntime 没 exclude 掉
// 傻逼 gradle 第二次配置 task 会覆盖掉第一次的配置
val x: com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.() -> Unit = {
dependencyFilter.exclude {
when ("${it.moduleGroup}:${it.moduleName}") {
"net.mamoe:mirai-core" -> true
"org.jetbrains.kotlin:kotlin-stdlib" -> true
"org.jetbrains.kotlin:kotlin-stdlib-jdk8" -> true
"net.mamoe:mirai-core-qqandroid" -> true
else -> false
}
}
}
set("shadowJar", x)
} }
tasks { tasks {
@ -120,14 +96,14 @@ tasks {
Regex("""val buildDate: Instant = Instant.ofEpochSecond\(.*\)""") Regex("""val buildDate: Instant = Instant.ofEpochSecond\(.*\)""")
) { ) {
"""val buildDate: Instant = Instant.ofEpochSecond(${ """val buildDate: Instant = Instant.ofEpochSecond(${
Instant.now().getEpochSecond() Instant.now().epochSecond
})""" })"""
} }
.replace( .replace(
Regex("""val version: Semver = Semver\(".*", Semver.SemverType.LOOSE\)""") Regex("""const val versionConst:\s+String\s+=\s+".*"""")
) { """val version: Semver = Semver("${project.version}", Semver.SemverType.LOOSE)""" } ) { """const val versionConst: String = "${project.version}"""" }
) )
} }
} }
} }
} }

View File

@ -12,12 +12,12 @@
package net.mamoe.mirai.console package net.mamoe.mirai.console
import com.vdurmont.semver4j.Semver
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.MiraiConsole.INSTANCE import net.mamoe.mirai.console.MiraiConsole.INSTANCE
import net.mamoe.mirai.console.MiraiConsoleImplementation.Companion.start import net.mamoe.mirai.console.MiraiConsoleImplementation.Companion.start
import net.mamoe.mirai.console.command.BuiltInCommands
import net.mamoe.mirai.console.extensions.BotConfigurationAlterer import net.mamoe.mirai.console.extensions.BotConfigurationAlterer
import net.mamoe.mirai.console.internal.MiraiConsoleImplementationBridge import net.mamoe.mirai.console.internal.MiraiConsoleImplementationBridge
import net.mamoe.mirai.console.internal.extension.GlobalComponentStorage import net.mamoe.mirai.console.internal.extension.GlobalComponentStorage
@ -28,6 +28,7 @@ import net.mamoe.mirai.console.plugin.loader.PluginLoader
import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.console.util.ConsoleInternalApi import net.mamoe.mirai.console.util.ConsoleInternalApi
import net.mamoe.mirai.console.util.CoroutineScopeUtils.childScopeContext import net.mamoe.mirai.console.util.CoroutineScopeUtils.childScopeContext
import net.mamoe.mirai.console.util.SemVersion
import net.mamoe.mirai.utils.BotConfiguration import net.mamoe.mirai.utils.BotConfiguration
import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.MiraiLogger
import java.io.File import java.io.File
@ -78,7 +79,7 @@ public interface MiraiConsole : CoroutineScope {
/** /**
* Console 后端版本号 * Console 后端版本号
*/ */
public val version: Semver public val version: SemVersion
@ConsoleExperimentalApi @ConsoleExperimentalApi
@ -146,6 +147,10 @@ public interface MiraiConsole : CoroutineScope {
else -> null!! else -> null!!
} }
} }
@ConsoleExperimentalApi("This is a low-level API and might be removed in the future.")
public val isActive: Boolean
get() = job.isActive
} }
} }

View File

@ -9,7 +9,8 @@
package net.mamoe.mirai.console package net.mamoe.mirai.console
import com.vdurmont.semver4j.Semver import net.mamoe.mirai.console.util.SemVersion
/** /**
* 有关前端实现的信息 * 有关前端实现的信息
@ -28,7 +29,7 @@ public interface MiraiConsoleFrontEndDescription {
/** /**
* 此前端实现的名称 * 此前端实现的名称
*/ */
public val version: Semver public val version: SemVersion
/** /**
* 兼容的 [MiraiConsole] 后端版本号 * 兼容的 [MiraiConsole] 后端版本号
@ -37,11 +38,10 @@ public interface MiraiConsoleFrontEndDescription {
* *
* 返回 `null` 表示禁止 [MiraiConsole] 后端检查版本兼容性. * 返回 `null` 表示禁止 [MiraiConsole] 后端检查版本兼容性.
*/ */
public val compatibleBackendVersion: Semver? get() = null public val compatibleBackendVersion: SemVersion? get() = null
/** /**
* 返回显示在 [MiraiConsole] 启动时的信息 * 返回显示在 [MiraiConsole] 启动时的信息
*/ */
@JvmDefault
public fun render(): String = "Frontend ${name}: version ${version}, provided by $vendor" public fun render(): String = "Frontend ${name}: version ${version}, provided by $vendor"
} }

View File

@ -108,12 +108,10 @@ public interface MiraiConsoleImplementation : CoroutineScope {
@JvmSynthetic @JvmSynthetic
@JvmDefault
public override suspend fun sendMessage(message: Message): Unit = public override suspend fun sendMessage(message: Message): Unit =
withContext(Dispatchers.IO) { sendMessageJ(message) } withContext(Dispatchers.IO) { sendMessageJ(message) }
@JvmSynthetic @JvmSynthetic
@JvmDefault
public override suspend fun sendMessage(message: String): Unit = public override suspend fun sendMessage(message: String): Unit =
withContext(Dispatchers.IO) { sendMessageJ(message) } withContext(Dispatchers.IO) { sendMessageJ(message) }
} }
@ -175,11 +173,21 @@ public interface MiraiConsoleImplementation : CoroutineScope {
internal lateinit var instance: MiraiConsoleImplementation internal lateinit var instance: MiraiConsoleImplementation
private val initLock = ReentrantLock() private val initLock = ReentrantLock()
/**
* 可由前端调用, 获取当前的 [MiraiConsoleImplementation] 实例
*
* 必须在 [start] 之后才能使用.
*/
@JvmStatic
@ConsoleFrontEndImplementation
public fun getInstance(): MiraiConsoleImplementation = instance
/** 由前端调用, 初始化 [MiraiConsole] 实例并启动 */ /** 由前端调用, 初始化 [MiraiConsole] 实例并启动 */
@JvmStatic @JvmStatic
@ConsoleFrontEndImplementation @ConsoleFrontEndImplementation
@Throws(MalformedMiraiConsoleImplementationError::class) @Throws(MalformedMiraiConsoleImplementationError::class)
public fun MiraiConsoleImplementation.start(): Unit = initLock.withLock { public fun MiraiConsoleImplementation.start(): Unit = initLock.withLock {
if (::instance.isInitialized) error("Mirai Console is already initialized.")
this@Companion.instance = this this@Companion.instance = this
MiraiConsoleImplementationBridge.doStart() MiraiConsoleImplementationBridge.doStart()
} }

View File

@ -0,0 +1,39 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.console.command
import net.mamoe.mirai.console.internal.command.createOrFindCommandPermission
import net.mamoe.mirai.console.permission.Permission
/**
* [Command] 的基础实现
*
* @see SimpleCommand
* @see CompositeCommand
* @see RawCommand
*/
public abstract class AbstractCommand
@JvmOverloads constructor(
public final override val owner: CommandOwner,
public final override val primaryName: String,
public final override val secondaryNames: Array<out String>,
public override val description: String = "<no description available>",
parentPermission: Permission = owner.parentPermission,
/** 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 */
public override val prefixOptional: Boolean = false,
) : Command {
init {
Command.checkCommandName(primaryName)
secondaryNames.forEach(Command.Companion::checkCommandName)
}
public override val usage: String get() = description
public override val permission: Permission by lazy { createOrFindCommandPermission(parentPermission) }
}

View File

@ -132,12 +132,12 @@ public object BuiltInCommands {
onFailure = { throwable -> onFailure = { throwable ->
sendMessage( sendMessage(
"Login failed: ${throwable.localizedMessage ?: throwable.message ?: throwable.toString()}" + "Login failed: ${throwable.localizedMessage ?: throwable.message ?: throwable.toString()}" +
if (this is CommandSenderOnMessage<*>) { if (this is CommandSenderOnMessage<*>) {
CommandManagerImpl.launch(CoroutineName("stacktrace delayer from Login")) { CommandManagerImpl.launch(CoroutineName("stacktrace delayer from Login")) {
fromEvent.nextMessageOrNull(60.secondsToMillis) { it.message.contentEquals("stacktrace") } fromEvent.nextMessageOrNull(60.secondsToMillis) { it.message.contentEquals("stacktrace") }
} }
"\n 1 分钟内发送 stacktrace 以获取堆栈信息" "\n 1 分钟内发送 stacktrace 以获取堆栈信息"
} else "" } else ""
) )
throw throwable throw throwable
@ -148,7 +148,7 @@ public object BuiltInCommands {
public object PermissionCommand : CompositeCommand( public object PermissionCommand : CompositeCommand(
ConsoleCommandOwner, "permission", "权限", "perm", ConsoleCommandOwner, "permission", "权限", "perm",
description = "Manage permissions", description = "管理权限",
overrideContext = buildCommandArgumentContext { overrideContext = buildCommandArgumentContext {
PermitteeId::class with PermitteeIdArgumentParser PermitteeId::class with PermitteeIdArgumentParser
Permission::class with PermissionIdArgumentParser.map { id -> Permission::class with PermissionIdArgumentParser.map { id ->
@ -159,30 +159,47 @@ public object BuiltInCommands {
}, },
), BuiltInCommandInternal { ), BuiltInCommandInternal {
// TODO: 2020/9/10 improve Permission command // TODO: 2020/9/10 improve Permission command
@Description("授权一个权限")
@SubCommand("permit", "grant", "add") @SubCommand("permit", "grant", "add")
public suspend fun CommandSender.permit(target: PermitteeId, permission: Permission) { public suspend fun CommandSender.permit(
@Name("被许可人 ID") target: PermitteeId,
@Name("权限 ID") permission: Permission,
) {
target.grantPermission(permission) target.grantPermission(permission)
sendMessage("OK") sendMessage("OK")
} }
@Description("取消授权一个权限")
@SubCommand("cancel", "deny", "remove") @SubCommand("cancel", "deny", "remove")
public suspend fun CommandSender.cancel(target: PermitteeId, permission: Permission) { public suspend fun CommandSender.cancel(
@Name("被许可人 ID") target: PermitteeId,
@Name("权限 ID") permission: Permission,
) {
target.denyPermission(permission, false) target.denyPermission(permission, false)
sendMessage("OK") sendMessage("OK")
} }
@Description("取消授权一个权限及其所有子权限")
@SubCommand("cancelAll", "denyAll", "removeAll") @SubCommand("cancelAll", "denyAll", "removeAll")
public suspend fun CommandSender.cancelAll(target: PermitteeId, permission: Permission) { public suspend fun CommandSender.cancelAll(
@Name("被许可人 ID") target: PermitteeId,
@Name("权限 ID") permission: Permission,
) {
target.denyPermission(permission, true) target.denyPermission(permission, true)
sendMessage("OK") sendMessage("OK")
} }
@Description("查看被授权权限列表")
@SubCommand("permittedPermissions", "pp", "grantedPermissions", "gp") @SubCommand("permittedPermissions", "pp", "grantedPermissions", "gp")
public suspend fun CommandSender.permittedPermissions(target: PermitteeId) { public suspend fun CommandSender.permittedPermissions(
@Name("被许可人 ID") target: PermitteeId,
) {
val grantedPermissions = target.getPermittedPermissions() val grantedPermissions = target.getPermittedPermissions()
sendMessage(grantedPermissions.joinToString("\n") { it.id.toString() }) sendMessage(grantedPermissions.joinToString("\n") { it.id.toString() })
} }
@Description("查看所有权限列表")
@SubCommand("listPermissions", "lp") @SubCommand("listPermissions", "lp")
public suspend fun CommandSender.listPermissions() { public suspend fun CommandSender.listPermissions() {
sendMessage(PermissionService.INSTANCE.getRegisteredPermissions().joinToString("\n") { it.id.toString() }) sendMessage(PermissionService.INSTANCE.getRegisteredPermissions().joinToString("\n") { it.id.toString() })

View File

@ -15,11 +15,11 @@ import net.mamoe.kjbb.JvmBlockingBridge
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register
import net.mamoe.mirai.console.command.java.JCommand import net.mamoe.mirai.console.command.java.JCommand
import net.mamoe.mirai.console.internal.command.createOrFindCommandPermission import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.internal.command.isValidSubName import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME
import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.console.permission.Permission
import net.mamoe.mirai.console.permission.PermissionId
import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.SingleMessage
/** /**
* 指令 * 指令
@ -34,11 +34,19 @@ import net.mamoe.mirai.message.data.SingleMessage
*/ */
public interface Command { public interface Command {
/** /**
* 指令名. 需要至少有一个元素. 所有元素都不能带有空格 * 主指令名. 将会参与构成 [Permission.id].
* *
* @see Command.primaryName 获取主要指令名 * 不允许包含 [空格][Char.isWhitespace], '.', ':'.
*/ */
public val names: Array<out String> @ResolveContext(COMMAND_NAME)
public val primaryName: String
/**
* 次要指令名
* @see Command.primaryName 获取主指令名
*/
@ResolveContext(COMMAND_NAME)
public val secondaryNames: Array<out String>
/** /**
* 用法说明, 用于发送给用户. [usage] 一般包含 [description]. * 用法说明, 用于发送给用户. [usage] 一般包含 [description].
@ -51,12 +59,18 @@ public interface Command {
public val description: String public val description: String
/** /**
* 指令权限 * 此指令所分配的权限.
*
* ### 实现约束
* - [Permission.id] 应由 [CommandOwner.permissionId] 创建. 因此保证相同的 [PermissionId.namespace]
* - [PermissionId.name] 应为 [主指令名][primaryName]
*/ */
public val permission: Permission public val permission: Permission
/** /**
* `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 * `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选.
*
* 会影响聊天语境中的解析.
*/ */
public val prefixOptional: Boolean public val prefixOptional: Boolean
@ -69,7 +83,7 @@ public interface Command {
/** /**
* 在指令被执行时调用. * 在指令被执行时调用.
* *
* @param args 指令参数. 数组元素类型可能是 [SingleMessage] [String]. 且已经以 ' ' 分割. * @param args 精确的指令参数. [MessageChain] 每个元素代表一个精确的参数.
* *
* @see CommandManager.executeCommand 查看更多信息 * @see CommandManager.executeCommand 查看更多信息
*/ */
@ -77,41 +91,37 @@ public interface Command {
public suspend fun CommandSender.onCommand(args: MessageChain) public suspend fun CommandSender.onCommand(args: MessageChain)
public companion object { public companion object {
/** /**
* 主要指令名. [Command.names] 的第一个元素. * 获取所有指令名称 (包含 [primaryName] [secondaryNames]).
*
* @return 数组大小至少为 1. 第一个元素总是 [primaryName]. 随后是保持原顺序的 [secondaryNames]
*/ */
@JvmStatic @JvmStatic
public val Command.primaryName: String public val Command.allNames: Array<String>
get() = names[0] get() = arrayOf(primaryName, *secondaryNames)
/**
* 检查指令名的合法性. 在非法时抛出 [IllegalArgumentException]
*/
@JvmStatic
@Throws(IllegalArgumentException::class)
public fun checkCommandName(@ResolveContext(COMMAND_NAME) name: String) {
when {
name.isBlank() -> throw IllegalArgumentException("Command name should not be blank.")
name.any { it.isWhitespace() } -> throw IllegalArgumentException("Spaces is not yet allowed in command name.")
name.contains(':') -> throw IllegalArgumentException("':' is forbidden in command name.")
name.contains('.') -> throw IllegalArgumentException("'.' is forbidden in command name.")
}
}
} }
} }
/**
* 调用 [Command.onCommand]
* @see Command.onCommand
*/
@JvmSynthetic @JvmSynthetic
public suspend inline fun Command.onCommand(sender: CommandSender, args: MessageChain): Unit = public suspend inline fun Command.onCommand(sender: CommandSender, args: MessageChain): Unit =
sender.onCommand(args) sender.onCommand(args)
/**
* [Command] 的基础实现
*
* @see SimpleCommand
* @see CompositeCommand
* @see RawCommand
*/
public abstract class AbstractCommand
@JvmOverloads constructor(
/** 指令拥有者. */
public override val owner: CommandOwner,
vararg names: String,
description: String = "<no description available>",
parentPermission: Permission = owner.parentPermission,
/** 为 `true` 时表示 [指令前缀][CommandManager.commandPrefix] 可选 */
public override val prefixOptional: Boolean = false,
) : Command {
public override val description: String = description.trimIndent()
public override val names: Array<out String> =
names.map(String::trim).filterNot(String::isEmpty).map(String::toLowerCase).also { list ->
list.firstOrNull { !it.isValidSubName() }?.let { error("Invalid name: $it") }
}.toTypedArray()
public override val permission: Permission by lazy { createOrFindCommandPermission(parentPermission) }
}

View File

@ -11,7 +11,6 @@
package net.mamoe.mirai.console.command package net.mamoe.mirai.console.command
import net.mamoe.mirai.console.command.Command.Companion.primaryName
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand
/** /**

View File

@ -26,11 +26,15 @@ import net.mamoe.mirai.message.data.*
public interface CommandManager { public interface CommandManager {
/** /**
* 获取已经注册了的属于这个 [CommandOwner] 的指令列表. * 获取已经注册了的属于这个 [CommandOwner] 的指令列表.
*
* @return 这一时刻的浅拷贝.
*/ */
public val CommandOwner.registeredCommands: List<Command> public val CommandOwner.registeredCommands: List<Command>
/** /**
* 获取所有已经注册了指令列表. * 获取所有已经注册了指令列表.
*
* @return 这一时刻的浅拷贝.
*/ */
public val allRegisteredCommands: List<Command> public val allRegisteredCommands: List<Command>
@ -49,8 +53,8 @@ public interface CommandManager {
* *
* @param override 是否覆盖重名指令. * @param override 是否覆盖重名指令.
* *
* 若原有指令 P, [Command.names] 'a', 'b', 'c'. * 若原有指令 P, [Command.secondaryNames] 'a', 'b', 'c'.
* 新指令 Q, [Command.names] 'b', 将会覆盖原指令 A 注册的 'b'. * 新指令 Q, [Command.secondaryNames] 'b', 将会覆盖原指令 A 注册的 'b'.
* *
* 即注册完成后, 'a' 'c' 将会解析到指令 P, 'b' 会解析到指令 Q. * 即注册完成后, 'a' 'c' 将会解析到指令 P, 'b' 会解析到指令 Q.
* *
@ -71,19 +75,23 @@ public interface CommandManager {
public fun Command.findDuplicate(): Command? public fun Command.findDuplicate(): Command?
/** /**
* 取消注册这个指令. 若指令未注册, 返回 `false`. * 取消注册这个指令.
*
* 若指令未注册, 返回 `false`.
*/ */
@JvmName("unregisterCommand") @JvmName("unregisterCommand")
public fun Command.unregister(): Boolean public fun Command.unregister(): Boolean
/** /**
* [this] 已经 [注册][register] 返回 `true` * [this] 已经 [注册][register] 返回 `true`
*/ */
@JvmName("isCommandRegistered") @JvmName("isCommandRegistered")
public fun Command.isRegistered(): Boolean public fun Command.isRegistered(): Boolean
/** /**
* 解析并执行一个指令 * 解析并执行一个指令.
*
* 如要避免参数解析, 请使用 [Command.onCommand]
* *
* ### 指令解析流程 * ### 指令解析流程
* 1. [message] 的第一个消息元素的 [内容][Message.contentToString] 被作为指令名, 在已注册指令列表中搜索. (包含 [Command.prefixOptional] 相关的处理) * 1. [message] 的第一个消息元素的 [内容][Message.contentToString] 被作为指令名, 在已注册指令列表中搜索. (包含 [Command.prefixOptional] 相关的处理)
@ -116,7 +124,6 @@ public interface CommandManager {
* @return 执行结果 * @return 执行结果
* @see executeCommand * @see executeCommand
*/ */
@JvmDefault
@JvmBlockingBridge @JvmBlockingBridge
public suspend fun CommandSender.executeCommand( public suspend fun CommandSender.executeCommand(
message: String, message: String,
@ -139,7 +146,6 @@ public interface CommandManager {
* 执行一个确切的指令 * 执行一个确切的指令
* @see executeCommand 获取更多信息 * @see executeCommand 获取更多信息
*/ */
@JvmDefault
@JvmBlockingBridge @JvmBlockingBridge
@JvmName("executeCommand") @JvmName("executeCommand")
public suspend fun Command.execute( public suspend fun Command.execute(
@ -151,7 +157,7 @@ public interface CommandManager {
public companion object INSTANCE : CommandManager by CommandManagerImpl { public companion object INSTANCE : CommandManager by CommandManagerImpl {
// TODO: 2020/8/20 https://youtrack.jetbrains.com/issue/KT-41191 // TODO: 2020/8/20 https://youtrack.jetbrains.com/issue/KT-41191
override val CommandOwner.registeredCommands: List<Command> get() = CommandManagerImpl.run { registeredCommands } override val CommandOwner.registeredCommands: List<Command> get() = CommandManagerImpl.run { this@registeredCommands.registeredCommands }
override fun CommandOwner.unregisterAllCommands(): Unit = CommandManagerImpl.run { unregisterAllCommands() } override fun CommandOwner.unregisterAllCommands(): Unit = CommandManagerImpl.run { unregisterAllCommands() }
override fun Command.register(override: Boolean): Boolean = CommandManagerImpl.run { register(override) } override fun Command.register(override: Boolean): Boolean = CommandManagerImpl.run { register(override) }
override fun Command.findDuplicate(): Command? = CommandManagerImpl.run { findDuplicate() } override fun Command.findDuplicate(): Command? = CommandManagerImpl.run { findDuplicate() }

View File

@ -10,6 +10,8 @@
package net.mamoe.mirai.console.command package net.mamoe.mirai.console.command
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.unregisterAllCommands import net.mamoe.mirai.console.command.CommandManager.INSTANCE.unregisterAllCommands
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.PERMISSION_NAME
import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.console.permission.Permission
import net.mamoe.mirai.console.permission.PermissionId import net.mamoe.mirai.console.permission.PermissionId
import net.mamoe.mirai.console.permission.PermissionIdNamespace import net.mamoe.mirai.console.permission.PermissionIdNamespace
@ -36,5 +38,7 @@ public interface CommandOwner : PermissionIdNamespace {
internal object ConsoleCommandOwner : CommandOwner { internal object ConsoleCommandOwner : CommandOwner {
override val parentPermission: Permission get() = BuiltInCommands.parentPermission override val parentPermission: Permission get() = BuiltInCommands.parentPermission
override fun permissionId(name: String): PermissionId = PermissionId("console", "command.$name") override fun permissionId(
@ResolveContext(PERMISSION_NAME) name: String,
): PermissionId = PermissionId("console", name)
} }

View File

@ -9,7 +9,6 @@
package net.mamoe.mirai.console.command package net.mamoe.mirai.console.command
import net.mamoe.mirai.console.command.Command.Companion.primaryName
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand
/** /**

View File

@ -171,7 +171,6 @@ public interface CommandSender : CoroutineScope, Permittee {
* *
* 对于 [MemberCommandSender], 这个函数总是发送给所在群 * 对于 [MemberCommandSender], 这个函数总是发送给所在群
*/ */
@JvmDefault
@JvmBlockingBridge @JvmBlockingBridge
public suspend fun sendMessage(message: String): MessageReceipt<Contact>? public suspend fun sendMessage(message: String): MessageReceipt<Contact>?
@ -501,7 +500,6 @@ public fun CommandSender.getBotOrNull(): Bot? {
* *
* 控制台拥有一切指令的执行权限. * 控制台拥有一切指令的执行权限.
*/ */
// 前端实现
public object ConsoleCommandSender : AbstractCommandSender() { public object ConsoleCommandSender : AbstractCommandSender() {
public const val NAME: String = "ConsoleCommandSender" public const val NAME: String = "ConsoleCommandSender"
@ -514,12 +512,15 @@ public object ConsoleCommandSender : AbstractCommandSender() {
public override val permitteeId: AbstractPermitteeId.Console = AbstractPermitteeId.Console public override val permitteeId: AbstractPermitteeId.Console = AbstractPermitteeId.Console
public override val coroutineContext: CoroutineContext by lazy { MiraiConsole.childScopeContext(NAME) } public override val coroutineContext: CoroutineContext by lazy { MiraiConsole.childScopeContext(NAME) }
@JvmBlockingBridge
public override suspend fun sendMessage(message: Message): Nothing? { public override suspend fun sendMessage(message: Message): Nothing? {
MiraiConsoleImplementationBridge.consoleCommandSender.sendMessage(message) MiraiConsoleImplementationBridge.consoleCommandSender.sendMessage(message)
return null return null
} }
public override suspend fun sendMessage(message: String): MessageReceipt<User>? { @JvmBlockingBridge
public override suspend fun sendMessage(message: String): Nothing? {
MiraiConsoleImplementationBridge.consoleCommandSender.sendMessage(message) MiraiConsoleImplementationBridge.consoleCommandSender.sendMessage(message)
return null return null
} }

View File

@ -18,9 +18,12 @@
package net.mamoe.mirai.console.command package net.mamoe.mirai.console.command
import net.mamoe.mirai.console.command.description.* import net.mamoe.mirai.console.command.description.*
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME
import net.mamoe.mirai.console.internal.command.AbstractReflectionCommand import net.mamoe.mirai.console.internal.command.AbstractReflectionCommand
import net.mamoe.mirai.console.internal.command.CompositeCommandSubCommandAnnotationResolver import net.mamoe.mirai.console.internal.command.CompositeCommandSubCommandAnnotationResolver
import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.console.permission.Permission
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageChain
import kotlin.annotation.AnnotationRetention.RUNTIME import kotlin.annotation.AnnotationRetention.RUNTIME
import kotlin.annotation.AnnotationTarget.FUNCTION import kotlin.annotation.AnnotationTarget.FUNCTION
@ -81,12 +84,13 @@ import kotlin.annotation.AnnotationTarget.FUNCTION
*/ */
public abstract class CompositeCommand( public abstract class CompositeCommand(
owner: CommandOwner, owner: CommandOwner,
vararg names: String, @ResolveContext(COMMAND_NAME) primaryName: String,
@ResolveContext(COMMAND_NAME) vararg secondaryNames: String,
description: String = "no description available", description: String = "no description available",
parentPermission: Permission = owner.parentPermission, parentPermission: Permission = owner.parentPermission,
prefixOptional: Boolean = false, prefixOptional: Boolean = false,
overrideContext: CommandArgumentContext = EmptyCommandArgumentContext, overrideContext: CommandArgumentContext = EmptyCommandArgumentContext,
) : Command, AbstractReflectionCommand(owner, names, description, parentPermission, prefixOptional), ) : Command, AbstractReflectionCommand(owner, primaryName, secondaryNames = secondaryNames, description, parentPermission, prefixOptional),
CommandArgumentContextAware { CommandArgumentContextAware {
/** /**
@ -105,7 +109,9 @@ public abstract class CompositeCommand(
*/ */
@Retention(RUNTIME) @Retention(RUNTIME)
@Target(FUNCTION) @Target(FUNCTION)
protected annotation class SubCommand(vararg val value: String) protected annotation class SubCommand(
@ResolveContext(COMMAND_NAME) vararg val value: String,
)
/** 指令描述 */ /** 指令描述 */
@Retention(RUNTIME) @Retention(RUNTIME)
@ -113,6 +119,7 @@ public abstract class CompositeCommand(
protected annotation class Description(val value: String) protected annotation class Description(val value: String)
/** 参数名, 将参与构成 [usage] */ /** 参数名, 将参与构成 [usage] */
@ConsoleExperimentalApi("Classname might change")
@Retention(RUNTIME) @Retention(RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER) @Target(AnnotationTarget.VALUE_PARAMETER)
protected annotation class Name(val value: String) protected annotation class Name(val value: String)

View File

@ -14,6 +14,8 @@ package net.mamoe.mirai.console.command
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.execute import net.mamoe.mirai.console.command.CommandManager.INSTANCE.execute
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand
import net.mamoe.mirai.console.command.java.JRawCommand import net.mamoe.mirai.console.command.java.JRawCommand
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME
import net.mamoe.mirai.console.internal.command.createOrFindCommandPermission import net.mamoe.mirai.console.internal.command.createOrFindCommandPermission
import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.console.permission.Permission
import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageChain
@ -35,11 +37,13 @@ public abstract class RawCommand(
* @see CommandOwner * @see CommandOwner
*/ */
public override val owner: CommandOwner, public override val owner: CommandOwner,
/** 指令名. 需要至少有一个元素. 所有元素都不能带有空格 */ /** 主指令名. */
public override vararg val names: String, @ResolveContext(COMMAND_NAME) public override val primaryName: String,
/** 次要指令名. */
@ResolveContext(COMMAND_NAME) public override vararg val secondaryNames: String,
/** 用法说明, 用于发送给用户 */ /** 用法说明, 用于发送给用户 */
public override val usage: String = "<no usages given>", public override val usage: String = "<no usages given>",
/** 指令描述, 用于显示在 [BuiltInCommands.Help] */ /** 指令描述, 用于显示在 [BuiltInCommands.HelpCommand] */
public override val description: String = "<no descriptions given>", public override val description: String = "<no descriptions given>",
/** 指令父权限 */ /** 指令父权限 */
parentPermission: Permission = owner.parentPermission, parentPermission: Permission = owner.parentPermission,

View File

@ -20,6 +20,8 @@ package net.mamoe.mirai.console.command
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand
import net.mamoe.mirai.console.command.description.* import net.mamoe.mirai.console.command.description.*
import net.mamoe.mirai.console.command.java.JSimpleCommand import net.mamoe.mirai.console.command.java.JSimpleCommand
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME
import net.mamoe.mirai.console.internal.command.AbstractReflectionCommand import net.mamoe.mirai.console.internal.command.AbstractReflectionCommand
import net.mamoe.mirai.console.internal.command.SimpleCommandSubCommandAnnotationResolver import net.mamoe.mirai.console.internal.command.SimpleCommandSubCommandAnnotationResolver
import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.console.permission.Permission
@ -50,12 +52,13 @@ import net.mamoe.mirai.message.data.MessageChain
*/ */
public abstract class SimpleCommand( public abstract class SimpleCommand(
owner: CommandOwner, owner: CommandOwner,
vararg names: String, @ResolveContext(COMMAND_NAME) primaryName: String,
@ResolveContext(COMMAND_NAME) vararg secondaryNames: String,
description: String = "no description available", description: String = "no description available",
parentPermission: Permission = owner.parentPermission, parentPermission: Permission = owner.parentPermission,
prefixOptional: Boolean = false, prefixOptional: Boolean = false,
overrideContext: CommandArgumentContext = EmptyCommandArgumentContext, overrideContext: CommandArgumentContext = EmptyCommandArgumentContext,
) : Command, AbstractReflectionCommand(owner, names, description, parentPermission, prefixOptional), ) : Command, AbstractReflectionCommand(owner, primaryName, secondaryNames = secondaryNames, description, parentPermission, prefixOptional),
CommandArgumentContextAware { CommandArgumentContextAware {
/** /**

View File

@ -20,6 +20,8 @@ import net.mamoe.mirai.console.permission.PermissionId
import net.mamoe.mirai.console.permission.PermitteeId import net.mamoe.mirai.console.permission.PermitteeId
import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.contact.* import net.mamoe.mirai.contact.*
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.PlainText
import kotlin.internal.LowPriorityInOverloadResolution import kotlin.internal.LowPriorityInOverloadResolution
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.isSubclassOf
@ -75,6 +77,9 @@ public interface CommandArgumentContext {
Double::class with DoubleArgumentParser Double::class with DoubleArgumentParser
Float::class with FloatArgumentParser Float::class with FloatArgumentParser
Image::class with ImageArgumentParser
PlainText::class with PlainTextArgumentParser
Contact::class with ExistingContactArgumentParser Contact::class with ExistingContactArgumentParser
User::class with ExistingUserArgumentParser User::class with ExistingUserArgumentParser
Member::class with ExistingMemberArgumentParser Member::class with ExistingMemberArgumentParser

View File

@ -77,7 +77,6 @@ public interface CommandArgumentParser<out T : Any> {
* @see CommandArgumentParserException * @see CommandArgumentParserException
*/ */
@Throws(CommandArgumentParserException::class) @Throws(CommandArgumentParserException::class)
@JvmDefault
public fun parse(raw: MessageContent, sender: CommandSender): T = parse(raw.content, sender) public fun parse(raw: MessageContent, sender: CommandSender): T = parse(raw.content, sender)
} }

View File

@ -19,10 +19,7 @@ import net.mamoe.mirai.console.permission.PermitteeId
import net.mamoe.mirai.contact.* import net.mamoe.mirai.contact.*
import net.mamoe.mirai.getFriendOrNull import net.mamoe.mirai.getFriendOrNull
import net.mamoe.mirai.getGroupOrNull import net.mamoe.mirai.getGroupOrNull
import net.mamoe.mirai.message.data.At import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.message.data.MessageContent
import net.mamoe.mirai.message.data.SingleMessage
import net.mamoe.mirai.message.data.content
/** /**
@ -80,15 +77,44 @@ public object StringArgumentParser : InternalCommandArgumentParserExtensions<Str
public override fun parse(raw: String, sender: CommandSender): String = raw public override fun parse(raw: String, sender: CommandSender): String = raw
} }
/**
* 解析 [String] 通过 [Image].
*/
public object ImageArgumentParser : InternalCommandArgumentParserExtensions<Image> {
public override fun parse(raw: String, sender: CommandSender): Image {
return kotlin.runCatching {
Image(raw)
}.getOrElse {
illegalArgument("无法解析 $raw 为图片.")
}
}
override fun parse(raw: MessageContent, sender: CommandSender): Image {
if (raw is Image) return raw
return super.parse(raw, sender)
}
}
public object PlainTextArgumentParser : InternalCommandArgumentParserExtensions<PlainText> {
public override fun parse(raw: String, sender: CommandSender): PlainText {
return PlainText(raw)
}
override fun parse(raw: MessageContent, sender: CommandSender): PlainText {
if (raw is PlainText) return raw
return super.parse(raw, sender)
}
}
/** /**
* 当字符串内容为(不区分大小写) "true", "yes", "enabled" * 当字符串内容为(不区分大小写) "true", "yes", "enabled"
*/ */
public object BooleanArgumentParser : InternalCommandArgumentParserExtensions<Boolean> { public object BooleanArgumentParser : InternalCommandArgumentParserExtensions<Boolean> {
public override fun parse(raw: String, sender: CommandSender): Boolean = raw.trim().let { str -> public override fun parse(raw: String, sender: CommandSender): Boolean = raw.trim().let { str ->
str.equals("true", ignoreCase = true) str.equals("true", ignoreCase = true)
|| str.equals("yes", ignoreCase = true) || str.equals("yes", ignoreCase = true)
|| str.equals("enabled", ignoreCase = true) || str.equals("enabled", ignoreCase = true)
|| str.equals("on", ignoreCase = true) || str.equals("on", ignoreCase = true)
} }
} }
@ -365,10 +391,10 @@ internal interface InternalCommandArgumentParserExtensions<T : Any> : CommandArg
} else { } else {
var index = 1 var index = 1
illegalArgument("无法找到成员 $idOrCard。 多个成员满足搜索结果或匹配度不足: \n\n" + illegalArgument("无法找到成员 $idOrCard。 多个成员满足搜索结果或匹配度不足: \n\n" +
candidates.joinToString("\n", limit = 6) { candidates.joinToString("\n", limit = 6) {
val percentage = (it.second * 100).toDecimalPlace(0) val percentage = (it.second * 100).toDecimalPlace(0)
"#${index++}(${percentage}%)${it.first.nameCardOrNick.truncate(10)}(${it.first.id})" // #1 15.4% "#${index++}(${percentage}%)${it.first.nameCardOrNick.truncate(10)}(${it.first.id})" // #1 15.4%
} }
) )
} }
} }

View File

@ -16,7 +16,6 @@ import net.mamoe.mirai.console.command.CommandManager
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand
import net.mamoe.mirai.console.command.CommandSender import net.mamoe.mirai.console.command.CommandSender
import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.SingleMessage
/** /**
* Java 用户添加协程帮助的 [Command]. * Java 用户添加协程帮助的 [Command].
@ -33,9 +32,9 @@ public interface JCommand : Command {
/** /**
* 在指令被执行时调用. * 在指令被执行时调用.
* *
* @param args 指令参数. 数组元素类型可能是 [SingleMessage] [String]. 且已经以 ' ' 分割. * @param args 精确的指令参数. [MessageChain] 每个元素代表一个精确的参数.
* *
* @see CommandManager.executeCommand 查看更多信息 * @see CommandManager.executeCommand 查看更多信息
*/ */
public fun onCommand(sender: CommandSender, args: MessageChain) // overrides bridge public fun onCommand(sender: CommandSender, args: MessageChain) // overrides blocking bridge
} }

View File

@ -14,6 +14,8 @@ import net.mamoe.mirai.console.command.CommandManager
import net.mamoe.mirai.console.command.CommandOwner import net.mamoe.mirai.console.command.CommandOwner
import net.mamoe.mirai.console.command.CompositeCommand import net.mamoe.mirai.console.command.CompositeCommand
import net.mamoe.mirai.console.command.description.buildCommandArgumentContext import net.mamoe.mirai.console.command.description.buildCommandArgumentContext
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME
import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.console.permission.Permission
/** /**
@ -24,7 +26,7 @@ import net.mamoe.mirai.console.permission.Permission
* public final class MyCompositeCommand extends CompositeCommand { * public final class MyCompositeCommand extends CompositeCommand {
* public static final MyCompositeCommand INSTANCE = new MyCompositeCommand(); * public static final MyCompositeCommand INSTANCE = new MyCompositeCommand();
* *
* public MyCompositeCommand() { * private MyCompositeCommand() {
* super(MyPluginMain.INSTANCE, "manage") // "manage" 是主指令名 * super(MyPluginMain.INSTANCE, "manage") // "manage" 是主指令名
* } * }
* *
@ -69,11 +71,12 @@ import net.mamoe.mirai.console.permission.Permission
public abstract class JCompositeCommand public abstract class JCompositeCommand
@JvmOverloads constructor( @JvmOverloads constructor(
owner: CommandOwner, owner: CommandOwner,
vararg names: String, @ResolveContext(COMMAND_NAME) primaryName: String,
@ResolveContext(COMMAND_NAME) vararg secondaryNames: String,
parentPermission: Permission = owner.parentPermission, parentPermission: Permission = owner.parentPermission,
) : CompositeCommand(owner, *names, parentPermission = parentPermission) { ) : CompositeCommand(owner, primaryName, secondaryNames = secondaryNames, parentPermission = parentPermission) {
/** 指令描述, 用于显示在 [BuiltInCommands.Help] */ /** 指令描述, 用于显示在 [BuiltInCommands.HelpCommand] */
public final override var description: String = "<no descriptions given>" public final override var description: String = "<no descriptions available>"
protected set protected set
public final override var permission: Permission = super.permission public final override var permission: Permission = super.permission

View File

@ -13,6 +13,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import net.mamoe.mirai.console.command.* import net.mamoe.mirai.console.command.*
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.execute import net.mamoe.mirai.console.command.CommandManager.INSTANCE.execute
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME
import net.mamoe.mirai.console.internal.command.createOrFindCommandPermission import net.mamoe.mirai.console.internal.command.createOrFindCommandPermission
import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.console.permission.Permission
import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageChain
@ -51,15 +53,15 @@ public abstract class JRawCommand
* @see CommandOwner * @see CommandOwner
*/ */
public override val owner: CommandOwner, public override val owner: CommandOwner,
/** 指令名. 需要至少有一个元素. 所有元素都不能带有空格 */ @ResolveContext(COMMAND_NAME) public override val primaryName: String,
public override vararg val names: String, @ResolveContext(COMMAND_NAME) public override vararg val secondaryNames: String,
parentPermission: Permission = owner.parentPermission, parentPermission: Permission = owner.parentPermission,
) : Command { ) : Command {
/** 用法说明, 用于发送给用户 */ /** 用法说明, 用于发送给用户 */
public override var usage: String = "<no usages given>" public override var usage: String = "<no usages given>"
protected set protected set
/** 指令描述, 用于显示在 [BuiltInCommands.Help] */ /** 指令描述, 用于显示在 [BuiltInCommands.HelpCommand] */
public final override var description: String = "<no descriptions given>" public final override var description: String = "<no descriptions given>"
protected set protected set

View File

@ -14,12 +14,15 @@ import net.mamoe.mirai.console.command.CommandManager.INSTANCE.executeCommand
import net.mamoe.mirai.console.command.CommandOwner import net.mamoe.mirai.console.command.CommandOwner
import net.mamoe.mirai.console.command.SimpleCommand import net.mamoe.mirai.console.command.SimpleCommand
import net.mamoe.mirai.console.command.description.CommandArgumentContext import net.mamoe.mirai.console.command.description.CommandArgumentContext
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME
import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.console.permission.Permission
/** /**
* Java 实现: * Java 实现:
* ```java * ```java
* public final class MySimpleCommand extends JSimpleCommand { * public final class MySimpleCommand extends JSimpleCommand {
* public static final MySimpleCommand INSTANCE = new MySimpleCommand();
* private MySimpleCommand() { * private MySimpleCommand() {
* super(MyPlugin.INSTANCE, "tell") * super(MyPlugin.INSTANCE, "tell")
* // 可选设置如下属性 * // 可选设置如下属性
@ -41,9 +44,10 @@ import net.mamoe.mirai.console.permission.Permission
*/ */
public abstract class JSimpleCommand( public abstract class JSimpleCommand(
owner: CommandOwner, owner: CommandOwner,
vararg names: String, @ResolveContext(COMMAND_NAME) primaryName: String,
@ResolveContext(COMMAND_NAME) vararg secondaryNames: String,
basePermission: Permission, basePermission: Permission,
) : SimpleCommand(owner, *names, parentPermission = basePermission) { ) : SimpleCommand(owner, primaryName, secondaryNames = secondaryNames, parentPermission = basePermission) {
public override var description: String = super.description public override var description: String = super.description
protected set protected set

View File

@ -0,0 +1,51 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("unused")
package net.mamoe.mirai.console.compiler.common
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import kotlin.annotation.AnnotationTarget.*
/**
* 标记一个参数的语境类型, 用于帮助编译器和 IntelliJ 插件进行语境推断.
*/
@ConsoleExperimentalApi
@Target(
VALUE_PARAMETER,
PROPERTY, FIELD,
FUNCTION,
TYPE, TYPE_PARAMETER
)
@Retention(AnnotationRetention.BINARY)
public annotation class ResolveContext(
val kind: Kind,
) {
/**
* 元素数量可能在任意时间被改动
*/
public enum class Kind {
///////////////////////////////////////////////////////////////////////////
// ConstantKind
///////////////////////////////////////////////////////////////////////////
PLUGIN_ID, // ILLEGAL_PLUGIN_DESCRIPTION
PLUGIN_NAME, // ILLEGAL_PLUGIN_DESCRIPTION
PLUGIN_VERSION, // ILLEGAL_PLUGIN_DESCRIPTION
COMMAND_NAME, // ILLEGAL_COMMAND_NAME
PERMISSION_NAMESPACE, // ILLEGAL_COMMAND_NAMESPACE
PERMISSION_NAME, // ILLEGAL_COMMAND_NAME
PERMISSION_ID, // ILLEGAL_COMMAND_ID
RESTRICTED_NO_ARG_CONSTRUCTOR, // NOT_CONSTRUCTABLE_TYPE
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.console.compiler.common
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import kotlin.annotation.AnnotationTarget.FUNCTION
/**
* 标记一个函数, 在其函数体内限制特定一些函数的使用.
*/
@ConsoleExperimentalApi
@Target(FUNCTION)
@Retention(AnnotationRetention.BINARY)
public annotation class RestrictedScope(
vararg val kinds: Kind,
) {
public enum class Kind {
PERMISSION_REGISTER, // ILLEGAL_PERMISSION_REGISTER_USE
COMMAND_REGISTER, // ILLEGAL_COMMAND_REGISTER_USE
}
}

View File

@ -7,13 +7,18 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * https://github.com/mamoe/mirai/blob/master/LICENSE
*/ */
@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "EXPOSED_SUPER_CLASS", "NOTHING_TO_INLINE") @file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "EXPOSED_SUPER_CLASS", "NOTHING_TO_INLINE", "unused")
package net.mamoe.mirai.console.data package net.mamoe.mirai.console.data
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import net.mamoe.mirai.console.data.PluginData.ValueNode import kotlinx.serialization.modules.EmptySerializersModule
import kotlinx.serialization.modules.SerializersModule
import net.mamoe.mirai.console.internal.data.PluginDataImpl import net.mamoe.mirai.console.internal.data.PluginDataImpl
import net.mamoe.mirai.console.internal.data.getAnnotationListForValueSerialization
import net.mamoe.mirai.console.internal.data.valueName
import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import kotlin.reflect.KProperty
/** /**
* [PluginData] 的默认实现. 支持使用 `by value()` 等委托方法创建 [Value] 并跟踪其改动. * [PluginData] 的默认实现. 支持使用 `by value()` 等委托方法创建 [Value] 并跟踪其改动.
@ -21,6 +26,11 @@ import net.mamoe.mirai.console.internal.data.PluginDataImpl
* @see PluginData * @see PluginData
*/ */
public abstract class AbstractPluginData : PluginData, PluginDataImpl() { public abstract class AbstractPluginData : PluginData, PluginDataImpl() {
/**
* 这个 [PluginData] 保存时使用的名称.
*/
public abstract override val saveName: String
/** /**
* 添加了追踪的 [ValueNode] 列表, 即通过 `by value` 初始化的属性列表. * 添加了追踪的 [ValueNode] 列表, 即通过 `by value` 初始化的属性列表.
* *
@ -28,22 +38,145 @@ public abstract class AbstractPluginData : PluginData, PluginDataImpl() {
* *
* @see provideDelegate * @see provideDelegate
*/ */
public override val valueNodes: MutableList<ValueNode<*>> = mutableListOf() @ConsoleExperimentalApi
public val valueNodes: MutableList<ValueNode<*>> = mutableListOf()
/** /**
* 供手动实现时值跟踪使用 ( Java 用户). 一般 Kotlin 用户需使用 [provideDelegate] * 供手动实现时值跟踪使用 ( Java 用户). 一般 Kotlin 用户需使用 [provideDelegate]
*/ */
public override fun <T : SerializerAwareValue<*>> T.track(valueName: String, annotations: List<Annotation>): T = @ConsoleExperimentalApi
apply { valueNodes.add(ValueNode(valueName, this, annotations, this.serializer)) } public fun <T : SerializerAwareValue<*>> track(
value: T,
/**
* 值名称.
*
* 如果属性带有 [ValueName], 则使用 [ValueName.value],
* 否则使用 [属性名称][KProperty.name]
*
* @see [ValueNode.value]
*/
valueName: String,
annotations: List<Annotation>,
): T = value.apply { this@AbstractPluginData.valueNodes.add(ValueNode(valueName, this, annotations, this.serializer)) }
/** /**
* 所有 [valueNodes] 更新和保存序列化器. 仅供内部使用 * 使用 `by value()` 时自动调用此方法, 添加对 [Value] 的值修改的跟踪, 并创建 [ValueNode] 加入 [valueNodes]
*/ */
public operator fun <T : SerializerAwareValue<*>> T.provideDelegate(
thisRef: Any?,
property: KProperty<*>,
): T = track(this, property.valueName, property.getAnnotationListForValueSerialization())
/**
* 所有 [valueNodes] 更新和保存序列化器.
*/
@ConsoleExperimentalApi
public final override val updaterSerializer: KSerializer<Unit> public final override val updaterSerializer: KSerializer<Unit>
get() = super.updaterSerializer get() = super.updaterSerializer
@ConsoleExperimentalApi
public override val serializersModule: SerializersModule = EmptySerializersModule
/** /**
* 当所属于这个 [PluginData] [Value] [][Value.value] 被修改时被调用. * 当所属于这个 [PluginData] [Value] [][Value.value] 被修改时被调用.
*/ */
public abstract override fun onValueChanged(value: Value<*>) @ConsoleExperimentalApi
public override fun onValueChanged(value: Value<*>) {
// no-op by default
}
/**
* 当这个 [PluginData] 被放入一个 [PluginDataStorage] 时调用
*/
@ConsoleExperimentalApi
public override fun onInit(owner: PluginDataHolder, storage: PluginDataStorage) {
// no-op by default
}
/**
* [track] 创建, 来自一个通过 `by value` 初始化的属性节点.
*/
@ConsoleExperimentalApi
public data class ValueNode<T>(
/**
* 节点名称.
*
* 如果属性带有 [ValueName], 则使用 [ValueName.value],
* 否则使用 [属性名称][KProperty.name]
*/
val valueName: String,
/**
* 属性值代理
*/
val value: Value<out T>,
/**
* 注解列表
*/
val annotations: List<Annotation>,
/**
* 属性值更新器
*/
val updaterSerializer: KSerializer<Unit>,
)
}
/**
* 获取这个 [KProperty] 委托的 [Value]
*
* 示例:
* ```
* object MyData : AutoSavePluginData(PluginMain) {
* val list: List<String> by value()
* }
*
* val value: Value<List<String>> = MyData.findBackingFieldValue(MyData::list)
* ```
*
* @see PluginData
*/
@ConsoleExperimentalApi
public fun <T> AbstractPluginData.findBackingFieldValue(property: KProperty<T>): Value<out T>? =
findBackingFieldValue(property.valueName)
/**
* 获取这个 [KProperty] 委托的 [Value]
*
* 示例:
* ```
* object MyData : AutoSavePluginData(PluginMain) {
* @ValueName("theList")
* val list: List<String> by value()
* val int: Int by value()
* }
*
* val value: Value<List<String>> = MyData.findBackingFieldValue("theList") // 需使用 @ValueName 标注的名称
* val intValue: Value<Int> = MyData.findBackingFieldValue("int")
* ```
*
* @see PluginData
*/
@ConsoleExperimentalApi
public fun <T> AbstractPluginData.findBackingFieldValue(propertyValueName: String): Value<out T>? {
@Suppress("UNCHECKED_CAST")
return this.valueNodes.find { it.valueName == propertyValueName }?.value as Value<out T>
}
/**
* 获取这个 [KProperty] 委托的 [Value]
*
* 示例:
* ```
* object MyData : AutoSavePluginData(PluginMain) {
* val list: List<String> by value()
* }
*
* val value: PluginData.ValueNode<List<String>> = MyData.findBackingFieldValueNode(MyData::list)
* ```
*
* @see PluginData
*/
@ConsoleExperimentalApi
public fun <T> AbstractPluginData.findBackingFieldValueNode(property: KProperty<T>): AbstractPluginData.ValueNode<out T>? {
@Suppress("UNCHECKED_CAST")
return this.valueNodes.find { it == property } as AbstractPluginData.ValueNode<out T>?
} }

View File

@ -23,4 +23,10 @@ import kotlinx.coroutines.Job
* @see PluginConfig * @see PluginConfig
* @see AutoSavePluginData * @see AutoSavePluginData
*/ */
public open class AutoSavePluginConfig : AutoSavePluginData(), PluginConfig public open class AutoSavePluginConfig : AutoSavePluginData, PluginConfig {
@Deprecated("请手动指定保存名称. 此构造器将在 1.0.0 删除", level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("AutoSavePluginConfig(\"把我改成保存名称\")"))
@Suppress("DEPRECATION_ERROR")
public constructor() : super()
public constructor(saveName: String) : super(saveName)
}

View File

@ -18,6 +18,7 @@ import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip
import net.mamoe.mirai.console.internal.plugin.updateWhen import net.mamoe.mirai.console.internal.plugin.updateWhen
import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.*
import kotlin.reflect.full.findAnnotation
/** /**
* 链接自动保存的 [PluginData]. * 链接自动保存的 [PluginData].
@ -29,14 +30,29 @@ import net.mamoe.mirai.utils.*
* @see PluginData * @see PluginData
*/ */
public open class AutoSavePluginData private constructor( public open class AutoSavePluginData private constructor(
@Suppress("UNUSED_PARAMETER") primaryConstructorMark: Any? // KEEP THIS PRIMARY CONSTRUCTOR FOR FUTURE USE: WE'LL SUPPORT SERIALIZERS_MODULE FOR POLYMORPHISM
@Suppress("UNUSED_PARAMETER") primaryConstructorMark: Any?,
) : AbstractPluginData() { ) : AbstractPluginData() {
private lateinit var owner_: AutoSavePluginDataHolder private lateinit var owner_: AutoSavePluginDataHolder
private val autoSaveIntervalMillis_: LongRange get() = owner_.autoSaveIntervalMillis private val autoSaveIntervalMillis_: LongRange get() = owner_.autoSaveIntervalMillis
private lateinit var storage_: PluginDataStorage private lateinit var storage_: PluginDataStorage
public constructor() : this(null) public final override val saveName: String
get() = _saveName
private lateinit var _saveName: String
public constructor(saveName: String) : this(null) {
_saveName = saveName
}
@Deprecated("请手动指定保存名称. 此构造器将在 1.0.0 删除", level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("AutoSavePluginData(\"把我改成保存名称\")"))
public constructor() : this(null) {
val clazz = this::class
_saveName = clazz.findAnnotation<ValueName>()?.value
?: clazz.qualifiedName
?: throw IllegalArgumentException("Cannot find a serial name for ${this::class}")
}
@ConsoleExperimentalApi @ConsoleExperimentalApi
override fun onInit(owner: PluginDataHolder, storage: PluginDataStorage) { override fun onInit(owner: PluginDataHolder, storage: PluginDataStorage) {
@ -57,7 +73,7 @@ public open class AutoSavePluginData private constructor(
?.let { return@invokeOnCompletion } ?.let { return@invokeOnCompletion }
MiraiConsole.mainLogger.error( MiraiConsole.mainLogger.error(
"An exception occurred when saving config ${this@AutoSavePluginData::class.qualifiedNameOrTip} " + "An exception occurred when saving config ${this@AutoSavePluginData::class.qualifiedNameOrTip} " +
"but CoroutineExceptionHandler not found in PluginDataHolder.coroutineContext for ${owner::class.qualifiedNameOrTip}", "but CoroutineExceptionHandler not found in PluginDataHolder.coroutineContext for ${owner::class.qualifiedNameOrTip}",
e e
) )
} }
@ -121,6 +137,7 @@ public open class AutoSavePluginData private constructor(
} }
} }
@ConsoleExperimentalApi
public final override fun onValueChanged(value: Value<*>) { public final override fun onValueChanged(value: Value<*>) {
debuggingLogger1.error { "onValueChanged: $value" } debuggingLogger1.error { "onValueChanged: $value" }
if (::owner_.isInitialized) { if (::owner_.isInitialized) {

View File

@ -37,4 +37,10 @@ import net.mamoe.mirai.console.data.java.JAutoSavePluginConfig
* *
* @see PluginData * @see PluginData
*/ */
public interface PluginConfig : PluginData public interface PluginConfig : PluginData {
/**
* 警告: [PluginConfig] 的实现处于实验性阶段.
*
* 自主实现 [PluginConfig] 将得不到兼容性保障. 请仅考虑使用 [AutoSavePluginConfig]
*/
}

View File

@ -18,20 +18,27 @@
package net.mamoe.mirai.console.data package net.mamoe.mirai.console.data
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.serializersModuleOf
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.RESTRICTED_NO_ARG_CONSTRUCTOR
import net.mamoe.mirai.console.data.java.JAutoSavePluginData import net.mamoe.mirai.console.data.java.JAutoSavePluginData
import net.mamoe.mirai.console.internal.data.* import net.mamoe.mirai.console.internal.data.createInstanceSmart
import net.mamoe.mirai.console.internal.data.typeOf0
import net.mamoe.mirai.console.internal.data.valueFromKTypeImpl
import net.mamoe.mirai.console.internal.data.valueImpl
import net.mamoe.mirai.console.plugin.jvm.AbstractJvmPlugin
import net.mamoe.mirai.console.plugin.jvm.JvmPlugin import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
import net.mamoe.mirai.console.plugin.jvm.reloadPluginData
import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import kotlin.internal.LowPriorityInOverloadResolution import kotlin.internal.LowPriorityInOverloadResolution
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.full.findAnnotation
/** /**
* 一个插件内部的, 对用户隐藏的数据对象. 可包含对多个 [Value] 的值变更的跟踪. * 一个插件内部的, 对用户隐藏的数据对象. 可包含对多个 [Value] 的值变更的跟踪. 典型的实现为 [AbstractPluginData].
* *
* [PluginData] 不涉及有关数据的存储, 而是只维护数据结构: [属性节点列表][valueNodes]. * [AbstractPluginData] 不涉及有关数据的存储, 而是只维护数据结构: [属性节点列表][AbstractPluginData.valueNodes].
* *
* 有关存储方案, 请查看 [PluginDataStorage]. * 有关存储方案, 请查看 [PluginDataStorage].
* *
@ -68,7 +75,7 @@ import kotlin.reflect.full.findAnnotation
* val theList: MutableList<String> = AccountPluginData.list * val theList: MutableList<String> = AccountPluginData.list
* ``` * ```
* *
* 但也注意, 不要存储 `AccountPluginData.list`. 它可能受不到值跟踪. 若必要存储, 请使用 [PluginData.findBackingFieldValue] * 但也注意, 不要存储 `AccountPluginData.list`. 它可能受不到值跟踪. 若必要存储, 请使用 [AbstractPluginData.findBackingFieldValue]
* *
* ### 使用 Java * ### 使用 Java
* *
@ -98,93 +105,36 @@ import kotlin.reflect.full.findAnnotation
* *
* 要查看详细的解释请查看 [docs/PluginData.md](https://github.com/mamoe/mirai-console/blob/master/docs/PluginData.md) * 要查看详细的解释请查看 [docs/PluginData.md](https://github.com/mamoe/mirai-console/blob/master/docs/PluginData.md)
* *
* @see JvmPlugin.reloadPluginData 通过 [JvmPlugin] 获取指定 [PluginData] 实例. * @see AbstractJvmPlugin.reloadPluginData 通过 [JvmPlugin] 获取指定 [PluginData] 实例.
* @see PluginDataStorage [PluginData] 存储仓库 * @see PluginDataStorage [PluginData] 存储仓库
* @see PluginDataExtensions 相关 [SerializerAwareValue] 映射函数 * @see PluginDataExtensions 相关 [SerializerAwareValue] 映射函数
*/ */
public interface PluginData { public interface PluginData {
/** /**
* 添加了追踪的 [ValueNode] 列表 (即使用 `by value()` 委托的属性), 即通过 `by value` 初始化的属性列表. * 这个 [PluginData] 保存时使用的名称.
*
* 他们的修改会被跟踪, 并触发 [onValueChanged].
*
* @see provideDelegate
* @see track
*/
public val valueNodes: MutableList<ValueNode<*>>
/**
* 这个 [PluginData] 保存时使用的名称. 默认通过 [ValueName] 获取, 否则使用 [类全名][KClass.qualifiedName] ( [Class.getCanonicalName])
*/ */
@ConsoleExperimentalApi @ConsoleExperimentalApi
public val saveName: String public val saveName: String
get() {
val clazz = this::class
return clazz.findAnnotation<ValueName>()?.value
?: clazz.qualifiedName
?: throw IllegalArgumentException("Cannot find a serial name for ${this::class}")
}
/**
* [provideDelegate] 创建, 来自一个通过 `by value` 初始化的属性节点.
*/
@ConsoleExperimentalApi @ConsoleExperimentalApi
public data class ValueNode<T>(
/**
* 节点名称.
*
* 如果属性带有 [ValueName], 则使用 [ValueName.value],
* 否则使用 [属性名称][KProperty.name]
*/
val valueName: String,
/**
* 属性值代理
*/
val value: Value<out T>,
/**
* 注解列表
*/
val annotations: List<Annotation>,
/**
* 属性值更新器
*/
val updaterSerializer: KSerializer<Unit>
)
/**
* 使用 `by value()` 时自动调用此方法, 添加对 [Value] 的值修改的跟踪, 并创建 [ValueNode] 加入 [valueNodes]
*/
public operator fun <T : SerializerAwareValue<*>> T.provideDelegate(
thisRef: Any?,
property: KProperty<*>
): T = track(property.valueName, property.getAnnotationListForValueSerialization())
/**
* 供手动实现时值跟踪使用 ( Java 用户). 一般 Kotlin 用户需使用 [provideDelegate]
*/
public fun <T : SerializerAwareValue<*>> T.track(
/**
* 值名称.
*
* 如果属性带有 [ValueName], 则使用 [ValueName.value],
* 否则使用 [属性名称][KProperty.name]
*
* @see [ValueNode.value]
*/
valueName: String,
annotations: List<Annotation>
): T
/**
* 所有 [valueNodes] 更新和保存序列化器. 仅供内部使用
*/
public val updaterSerializer: KSerializer<Unit> public val updaterSerializer: KSerializer<Unit>
/** /**
* 当所属于这个 [PluginData] [Value] [][Value.value] 被修改时被调用. * 当所属于这个 [PluginData] [Value] [][Value.value] 被修改时被调用.
* 调用者为 [Value] 的实现.
*/ */
@ConsoleExperimentalApi
public fun onValueChanged(value: Value<*>) public fun onValueChanged(value: Value<*>)
/**
* 用于支持多态序列化.
*
* @see SerializersModule
* @see serializersModuleOf
*/
@ConsoleExperimentalApi
public val serializersModule: SerializersModule
/** /**
* 当这个 [PluginData] 被放入一个 [PluginDataStorage] 时调用 * 当这个 [PluginData] 被放入一个 [PluginDataStorage] 时调用
*/ */
@ -192,62 +142,6 @@ public interface PluginData {
public fun onInit(owner: PluginDataHolder, storage: PluginDataStorage) public fun onInit(owner: PluginDataHolder, storage: PluginDataStorage)
} }
/**
* 获取这个 [KProperty] 委托的 [Value]
*
* , 对于
* ```
* object MyData : AutoSavePluginData(PluginMain) {
* val list: List<String> by value()
* }
*
* val value: Value<List<String>> = MyData.findBackingFieldValue(MyData::list)
* ```
*
* @see PluginData
*/
public fun <T> PluginData.findBackingFieldValue(property: KProperty<T>): Value<out T>? =
findBackingFieldValue(property.valueName)
/**
* 获取这个 [KProperty] 委托的 [Value]
*
* , 对于
* ```
* object MyData : AutoSavePluginData(PluginMain) {
* @ValueName("theList")
* val list: List<String> by value()
* val int: Int by value()
* }
*
* val value: Value<List<String>> = MyData.findBackingFieldValue("theList") // 需使用 @ValueName 标注的名称
* val intValue: Value<Int> = MyData.findBackingFieldValue("int")
* ```
*
* @see PluginData
*/
public fun <T> PluginData.findBackingFieldValue(propertyValueName: String): Value<out T>? {
return this.valueNodes.find { it.valueName == propertyValueName }?.value as Value<T>
}
/**
* 获取这个 [KProperty] 委托的 [Value]
*
* , 对于
* ```
* object MyData : AutoSavePluginData(PluginMain) {
* val list: List<String> by value()
* }
*
* val value: PluginData.ValueNode<List<String>> = MyData.findBackingFieldValueNode(MyData::list)
* ```
*
* @see PluginData
*/
public fun <T> PluginData.findBackingFieldValueNode(property: KProperty<T>): PluginData.ValueNode<out T>? {
return this.valueNodes.find { it == property } as PluginData.ValueNode<out T>?
}
// don't default = 0, cause ambiguity // don't default = 0, cause ambiguity
//// region PluginData_value_primitives CODEGEN //// //// region PluginData_value_primitives CODEGEN ////
@ -313,7 +207,7 @@ public fun PluginData.value(default: String): SerializerAwareValue<String> = val
@LowPriorityInOverloadResolution @LowPriorityInOverloadResolution
public inline fun <reified T> PluginData.value( public inline fun <reified T> PluginData.value(
default: T, default: T,
crossinline apply: T.() -> Unit = {} crossinline apply: T.() -> Unit = {},
): SerializerAwareValue<T> = ): SerializerAwareValue<T> =
valueFromKType(typeOf0<T>(), default).also { it.value.apply() } valueFromKType(typeOf0<T>(), default).also { it.value.apply() }
@ -321,9 +215,10 @@ public inline fun <reified T> PluginData.value(
* 通过具体化类型创建一个 [SerializerAwareValue]. * 通过具体化类型创建一个 [SerializerAwareValue].
* @see valueFromKType 查看更多实现信息 * @see valueFromKType 查看更多实现信息
*/ */
@ResolveContext(RESTRICTED_NO_ARG_CONSTRUCTOR)
@LowPriorityInOverloadResolution @LowPriorityInOverloadResolution
public inline fun <reified T> PluginData.value(apply: T.() -> Unit = {}): SerializerAwareValue<T> = public inline fun <@ResolveContext(RESTRICTED_NO_ARG_CONSTRUCTOR) reified T>
valueImpl<T>(typeOf0<T>(), T::class).also { it.value.apply() } PluginData.value(apply: T.() -> Unit = {}): SerializerAwareValue<T> = valueImpl<T>(typeOf0<T>(), T::class).also { it.value.apply() }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@PublishedApi @PublishedApi

View File

@ -41,7 +41,6 @@ public interface PluginDataHolder {
* @see Companion.newPluginDataInstance * @see Companion.newPluginDataInstance
* @see KClass.createType * @see KClass.createType
*/ */
@JvmDefault
public fun <T : PluginData> newPluginDataInstance(type: KType): T = public fun <T : PluginData> newPluginDataInstance(type: KType): T =
newPluginDataInstanceUsingReflection<PluginData>(type) as T newPluginDataInstanceUsingReflection<PluginData>(type) as T
@ -64,7 +63,6 @@ public interface AutoSavePluginDataHolder : PluginDataHolder, CoroutineScope {
/** /**
* 仅支持确切的 [PluginData] 类型 * 仅支持确切的 [PluginData] 类型
*/ */
@JvmDefault
public override fun <T : PluginData> newPluginDataInstance(type: KType): T { public override fun <T : PluginData> newPluginDataInstance(type: KType): T {
val classifier = type.classifier?.cast<KClass<PluginData>>() val classifier = type.classifier?.cast<KClass<PluginData>>()
require(classifier != null && classifier.java == PluginData::class.java) { require(classifier != null && classifier.java == PluginData::class.java) {

View File

@ -135,6 +135,7 @@ public interface MultiFilePluginDataStorage : PluginDataStorage {
} }
} }
@ConsoleExperimentalApi
@get:JvmSynthetic @get:JvmSynthetic
public inline val MultiFilePluginDataStorage.directory: File public inline val MultiFilePluginDataStorage.directory: File
get() = this.directoryPath.toFile() get() = this.directoryPath.toFile()

View File

@ -14,7 +14,7 @@ package net.mamoe.mirai.console.data
* *
* : * :
* ``` * ```
* object AccountPluginData : PluginData by ... { * object AccountPluginData : AutoSavePluginData() {
* @ValueName("info") * @ValueName("info")
* val map: Map<String, String> by value("a" to "b") * val map: Map<String, String> by value("a" to "b")
* } * }
@ -26,6 +26,9 @@ package net.mamoe.mirai.console.data
* map: * map:
* a: b * a: b
* ``` * ```
*
* @see PluginData
* @see Value
*/ */
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) @Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)

View File

@ -37,4 +37,10 @@ import net.mamoe.mirai.console.data.PluginData
* @see JAutoSavePluginData * @see JAutoSavePluginData
* @see PluginConfig * @see PluginConfig
*/ */
public abstract class JAutoSavePluginConfig : AutoSavePluginConfig(), PluginConfig public abstract class JAutoSavePluginConfig : AutoSavePluginConfig, PluginConfig {
@Deprecated("请手动指定保存名称. 此构造器将在 1.0.0 删除", level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("JAutoSavePluginConfig(\"把我改成保存名称\")"))
@Suppress("DEPRECATION_ERROR")
public constructor() : super()
public constructor(saveName: String) : super(saveName)
}

View File

@ -66,7 +66,13 @@ import kotlin.reflect.full.createType
* *
* @see PluginData * @see PluginData
*/ */
public abstract class JAutoSavePluginData : AutoSavePluginData(), PluginConfig { public abstract class JAutoSavePluginData : AutoSavePluginData, PluginConfig {
@Deprecated("请手动指定保存名称. 此构造器将在 1.0.0 删除", level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("JAutoSavePluginData(\"把我改成保存名称\")"))
@Suppress("DEPRECATION_ERROR")
public constructor() : super()
public constructor(saveName: String) : super(saveName)
//// region JPluginData_value_primitives CODEGEN //// //// region JPluginData_value_primitives CODEGEN ////
/** /**

View File

@ -9,13 +9,14 @@
package net.mamoe.mirai.console.internal package net.mamoe.mirai.console.internal
import com.vdurmont.semver4j.Semver import net.mamoe.mirai.console.util.SemVersion
import java.time.Instant import java.time.Instant
internal object MiraiConsoleBuildConstants { // auto-filled on build (task :mirai-console:fillBuildConstants) internal object MiraiConsoleBuildConstants { // auto-filled on build (task :mirai-console:fillBuildConstants)
@JvmStatic @JvmStatic
val buildDate: Instant = Instant.ofEpochSecond(1599934775) val buildDate: Instant = Instant.ofEpochSecond(1601134282)
const val versionConst: String = "1.0-RC-dev-29"
@JvmStatic @JvmStatic
val version: Semver = Semver("1.0-M4", Semver.SemverType.LOOSE) val version: SemVersion = SemVersion(versionConst)
} }

View File

@ -11,7 +11,6 @@
package net.mamoe.mirai.console.internal package net.mamoe.mirai.console.internal
import com.vdurmont.semver4j.Semver
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -23,7 +22,6 @@ import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.MiraiConsoleFrontEndDescription import net.mamoe.mirai.console.MiraiConsoleFrontEndDescription
import net.mamoe.mirai.console.MiraiConsoleImplementation import net.mamoe.mirai.console.MiraiConsoleImplementation
import net.mamoe.mirai.console.command.BuiltInCommands import net.mamoe.mirai.console.command.BuiltInCommands
import net.mamoe.mirai.console.command.Command.Companion.primaryName
import net.mamoe.mirai.console.command.CommandManager import net.mamoe.mirai.console.command.CommandManager
import net.mamoe.mirai.console.command.ConsoleCommandSender import net.mamoe.mirai.console.command.ConsoleCommandSender
import net.mamoe.mirai.console.data.PluginDataStorage import net.mamoe.mirai.console.data.PluginDataStorage
@ -48,6 +46,7 @@ import net.mamoe.mirai.console.plugin.jvm.AbstractJvmPlugin
import net.mamoe.mirai.console.plugin.loader.PluginLoader import net.mamoe.mirai.console.plugin.loader.PluginLoader
import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.console.util.ConsoleInput import net.mamoe.mirai.console.util.ConsoleInput
import net.mamoe.mirai.console.util.SemVersion
import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.*
import java.nio.file.Path import java.nio.file.Path
import java.time.Instant import java.time.Instant
@ -67,7 +66,7 @@ internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleI
private val instance: MiraiConsoleImplementation by MiraiConsoleImplementation.Companion::instance private val instance: MiraiConsoleImplementation by MiraiConsoleImplementation.Companion::instance
override val buildDate: Instant by MiraiConsoleBuildConstants::buildDate override val buildDate: Instant by MiraiConsoleBuildConstants::buildDate
override val version: Semver by MiraiConsoleBuildConstants::version override val version: SemVersion by MiraiConsoleBuildConstants::version
override val rootPath: Path by instance::rootPath override val rootPath: Path by instance::rootPath
override val frontEndDescription: MiraiConsoleFrontEndDescription by instance::frontEndDescription override val frontEndDescription: MiraiConsoleFrontEndDescription by instance::frontEndDescription
@ -191,6 +190,10 @@ internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleI
PluginManagerImpl.enableAllLoadedPlugins() PluginManagerImpl.enableAllLoadedPlugins()
for (registeredCommand in CommandManager.allRegisteredCommands) {
registeredCommand.permission // init
}
mainLogger.info { "${PluginManagerImpl.plugins.size} plugin(s) enabled." } mainLogger.info { "${PluginManagerImpl.plugins.size} plugin(s) enabled." }
} }
@ -221,9 +224,9 @@ internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleI
} }
@Suppress("SpellCheckingInspection") @Suppress("SpellCheckingInspection")
@Retention(AnnotationRetention.SOURCE) @Retention(AnnotationRetention.BINARY)
@DslMarker @DslMarker
private annotation class ILoveOmaeKumikoForever internal annotation class ILoveOmaeKumikoForever
@ILoveOmaeKumikoForever @ILoveOmaeKumikoForever
private inline fun phase(block: () -> Unit) { private inline fun phase(block: () -> Unit) {

View File

@ -14,7 +14,7 @@ import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.command.* import net.mamoe.mirai.console.command.*
import net.mamoe.mirai.console.command.Command.Companion.primaryName import net.mamoe.mirai.console.command.Command.Companion.allNames
import net.mamoe.mirai.console.command.CommandSender.Companion.toCommandSender import net.mamoe.mirai.console.command.CommandSender.Companion.toCommandSender
import net.mamoe.mirai.event.Listener import net.mamoe.mirai.event.Listener
import net.mamoe.mirai.event.subscribeAlways import net.mamoe.mirai.event.subscribeAlways
@ -31,8 +31,9 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine
MiraiConsole.createLogger("command") MiraiConsole.createLogger("command")
} }
@Suppress("ObjectPropertyName")
@JvmField @JvmField
internal val registeredCommands: MutableList<Command> = mutableListOf() internal val _registeredCommands: MutableList<Command> = mutableListOf()
@JvmField @JvmField
internal val requiredPrefixCommandMap: MutableMap<String, Command> = mutableMapOf() internal val requiredPrefixCommandMap: MutableMap<String, Command> = mutableMapOf()
@ -89,8 +90,8 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine
///// IMPL ///// IMPL
override val CommandOwner.registeredCommands: List<Command> get() = CommandManagerImpl.registeredCommands.filter { it.owner == this } override val CommandOwner.registeredCommands: List<Command> get() = _registeredCommands.filter { it.owner == this }
override val allRegisteredCommands: List<Command> get() = registeredCommands.toList() // copy override val allRegisteredCommands: List<Command> get() = _registeredCommands.toList() // copy
override val commandPrefix: String get() = "/" override val commandPrefix: String get() = "/"
override fun CommandOwner.unregisterAllCommands() { override fun CommandOwner.unregisterAllCommands() {
for (registeredCommand in registeredCommands) { for (registeredCommand in registeredCommands) {
@ -100,24 +101,28 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine
override fun Command.register(override: Boolean): Boolean { override fun Command.register(override: Boolean): Boolean {
if (this is CompositeCommand) this.subCommands // init lazy if (this is CompositeCommand) this.subCommands // init lazy
this.permission // init lazy kotlin.runCatching {
this.names // init lazy this.permission // init lazy
this.description // init lazy this.secondaryNames // init lazy
this.usage // init lazy this.description // init lazy
this.usage // init lazy
}.onFailure {
throw IllegalStateException("Failed to init command ${this@register}.", it)
}
modifyLock.withLock { modifyLock.withLock {
if (!override) { if (!override) {
if (findDuplicate() != null) return false if (findDuplicate() != null) return false
} }
registeredCommands.add(this@register) _registeredCommands.add(this@register)
if (this.prefixOptional) { if (this.prefixOptional) {
for (name in this.names) { for (name in this.allNames) {
val lowerCaseName = name.toLowerCase() val lowerCaseName = name.toLowerCase()
optionalPrefixCommandMap[lowerCaseName] = this optionalPrefixCommandMap[lowerCaseName] = this
requiredPrefixCommandMap[lowerCaseName] = this requiredPrefixCommandMap[lowerCaseName] = this
} }
} else { } else {
for (name in this.names) { for (name in this.allNames) {
val lowerCaseName = name.toLowerCase() val lowerCaseName = name.toLowerCase()
optionalPrefixCommandMap.remove(lowerCaseName) // ensure resolution consistency optionalPrefixCommandMap.remove(lowerCaseName) // ensure resolution consistency
requiredPrefixCommandMap[lowerCaseName] = this requiredPrefixCommandMap[lowerCaseName] = this
@ -128,21 +133,21 @@ internal object CommandManagerImpl : CommandManager, CoroutineScope by Coroutine
} }
override fun Command.findDuplicate(): Command? = override fun Command.findDuplicate(): Command? =
registeredCommands.firstOrNull { it.names intersectsIgnoringCase this.names } _registeredCommands.firstOrNull { it.allNames intersectsIgnoringCase this.allNames }
override fun Command.unregister(): Boolean = modifyLock.withLock { override fun Command.unregister(): Boolean = modifyLock.withLock {
if (this.prefixOptional) { if (this.prefixOptional) {
this.names.forEach { this.allNames.forEach {
optionalPrefixCommandMap.remove(it) optionalPrefixCommandMap.remove(it)
} }
} }
this.names.forEach { this.allNames.forEach {
requiredPrefixCommandMap.remove(it) requiredPrefixCommandMap.remove(it)
} }
registeredCommands.remove(this) _registeredCommands.remove(this)
} }
override fun Command.isRegistered(): Boolean = this in registeredCommands override fun Command.isRegistered(): Boolean = this in _registeredCommands
override suspend fun Command.execute( override suspend fun Command.execute(
sender: CommandSender, sender: CommandSender,

View File

@ -12,7 +12,6 @@
package net.mamoe.mirai.console.internal.command package net.mamoe.mirai.console.internal.command
import net.mamoe.mirai.console.command.* import net.mamoe.mirai.console.command.*
import net.mamoe.mirai.console.command.Command.Companion.primaryName
import net.mamoe.mirai.console.command.description.CommandArgumentContext import net.mamoe.mirai.console.command.description.CommandArgumentContext
import net.mamoe.mirai.console.command.description.CommandArgumentContextAware import net.mamoe.mirai.console.command.description.CommandArgumentContextAware
import net.mamoe.mirai.console.internal.data.kClassQualifiedNameOrTip import net.mamoe.mirai.console.internal.data.kClassQualifiedNameOrTip
@ -42,19 +41,21 @@ internal object SimpleCommandSubCommandAnnotationResolver :
function.hasAnnotation<SimpleCommand.Handler>() function.hasAnnotation<SimpleCommand.Handler>()
override fun getSubCommandNames(baseCommand: AbstractReflectionCommand, function: KFunction<*>): Array<out String> = override fun getSubCommandNames(baseCommand: AbstractReflectionCommand, function: KFunction<*>): Array<out String> =
baseCommand.names baseCommand.secondaryNames
} }
internal abstract class AbstractReflectionCommand internal abstract class AbstractReflectionCommand
@JvmOverloads constructor( @JvmOverloads constructor(
owner: CommandOwner, owner: CommandOwner,
names: Array<out String>, primaryName: String,
secondaryNames: Array<out String>,
description: String = "<no description available>", description: String = "<no description available>",
parentPermission: Permission = owner.parentPermission, parentPermission: Permission = owner.parentPermission,
prefixOptional: Boolean = false, prefixOptional: Boolean = false,
) : Command, AbstractCommand( ) : Command, AbstractCommand(
owner, owner,
names = names, primaryName = primaryName,
secondaryNames = secondaryNames,
description = description, description = description,
parentPermission = parentPermission, parentPermission = parentPermission,
prefixOptional = prefixOptional prefixOptional = prefixOptional
@ -251,7 +252,7 @@ internal fun AbstractReflectionCommand.SubCommandDescriptor.createUsage(baseComm
internal fun AbstractReflectionCommand.createSubCommand( internal fun AbstractReflectionCommand.createSubCommand(
function: KFunction<*>, function: KFunction<*>,
context: CommandArgumentContext context: CommandArgumentContext,
): AbstractReflectionCommand.SubCommandDescriptor { ): AbstractReflectionCommand.SubCommandDescriptor {
val notStatic = !function.hasAnnotation<JvmStatic>() val notStatic = !function.hasAnnotation<JvmStatic>()
//val overridePermission = null//function.findAnnotation<CompositeCommand.PermissionId>()//optional //val overridePermission = null//function.findAnnotation<CompositeCommand.PermissionId>()//optional

View File

@ -10,7 +10,6 @@
package net.mamoe.mirai.console.internal.command package net.mamoe.mirai.console.internal.command
import net.mamoe.mirai.console.command.Command import net.mamoe.mirai.console.command.Command
import net.mamoe.mirai.console.command.Command.Companion.primaryName
import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.console.permission.Permission
import net.mamoe.mirai.console.permission.PermissionService import net.mamoe.mirai.console.permission.PermissionService
import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.Group

View File

@ -9,10 +9,9 @@
package net.mamoe.mirai.console.internal.data package net.mamoe.mirai.console.internal.data
import net.mamoe.mirai.console.data.MultiFilePluginDataStorage import kotlinx.serialization.json.Json
import net.mamoe.mirai.console.data.PluginData import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.data.PluginDataHolder import net.mamoe.mirai.console.data.*
import net.mamoe.mirai.console.data.PluginDataStorage
import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip
import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.MiraiLogger
@ -42,7 +41,7 @@ internal open class MultiFilePluginDataStorageImpl(
} else { } else {
this.store(holder, instance) // save an initial copy this.store(holder, instance) // save an initial copy
} }
logger.debug { "Successfully loaded PluginData: ${instance.saveName} (containing ${instance.valueNodes.size} properties)" } logger.debug { "Successfully loaded PluginData: ${instance.saveName} (containing ${instance.castOrNull<AbstractPluginData>()?.valueNodes?.size} properties)" }
} }
protected open fun getPluginDataFile(holder: PluginDataHolder, instance: PluginData): File { protected open fun getPluginDataFile(holder: PluginDataHolder, instance: PluginData): File {
@ -62,28 +61,33 @@ internal open class MultiFilePluginDataStorageImpl(
return file.toFile().also { it.createNewFile() } return file.toFile().also { it.createNewFile() }
} }
private val json = Json {
prettyPrint = true
ignoreUnknownKeys = true
isLenient = true
allowStructuredMapKeys = true
}
private val yaml = Yaml.default
@ConsoleExperimentalApi @ConsoleExperimentalApi
public override fun store(holder: PluginDataHolder, instance: PluginData) { public override fun store(holder: PluginDataHolder, instance: PluginData) {
val yaml =/* if (instance.saveName == "PermissionService") Json {
prettyPrint = true
ignoreUnknownKeys = true
isLenient = true
allowStructuredMapKeys = true
} /*Yaml(
configuration = YamlConfiguration(
mapSerialization = YamlConfiguration.MapSerialization.FLOW_MAP,
listSerialization = YamlConfiguration.ListSerialization.FLOW_SEQUENCE,
classSerialization = YamlConfiguration.MapSerialization.FLOW_MAP
)
)*/ else */Yaml.default
getPluginDataFile(holder, instance).writeText( getPluginDataFile(holder, instance).writeText(
kotlin.runCatching { kotlin.runCatching {
yaml.encodeToString(instance.updaterSerializer, Unit) yaml.encodeToString(instance.updaterSerializer, Unit)
}.recoverCatching {
// Just use mainLogger for convenience.
MiraiConsole.mainLogger.warning(
"Could not save ${instance.saveName} in YAML format due to exception in YAML encoder. " +
"Please report this exception and relevant configurations to https://github.com/mamoe/mirai-console/issues/new",
it
)
json.encodeToString(instance.updaterSerializer, Unit)
}.getOrElse { }.getOrElse {
throw IllegalStateException("Exception while saving $instance, saveName=${instance.saveName}", it) throw IllegalStateException("Exception while saving $instance, saveName=${instance.saveName}", it)
} }
) )
logger.debug { "Successfully saved PluginData: ${instance.saveName} (containing ${instance.valueNodes.size} properties)" } logger.debug { "Successfully saved PluginData: ${instance.saveName} (containing ${instance.castOrNull<AbstractPluginData>()?.valueNodes?.size} properties)" }
} }
} }

View File

@ -18,9 +18,9 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.CompositeDecoder import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
import net.mamoe.mirai.console.data.AbstractPluginData
import net.mamoe.mirai.console.data.AbstractPluginData.ValueNode
import net.mamoe.mirai.console.data.PluginData import net.mamoe.mirai.console.data.PluginData
import net.mamoe.mirai.console.data.PluginData.ValueNode
import net.mamoe.mirai.console.data.Value
import net.mamoe.mirai.console.data.ValueDescription import net.mamoe.mirai.console.data.ValueDescription
import net.mamoe.mirai.console.data.ValueName import net.mamoe.mirai.console.data.ValueName
import net.mamoe.yamlkt.Comment import net.mamoe.yamlkt.Comment
@ -34,12 +34,25 @@ import kotlin.reflect.KAnnotatedElement
* - Auto-saving * - Auto-saving
*/ */
internal abstract class PluginDataImpl { internal abstract class PluginDataImpl {
internal fun findNodeInstance(name: String): ValueNode<*>? = valueNodes.firstOrNull { it.valueName == name } init {
@Suppress("LeakingThis")
check(this is AbstractPluginData)
}
internal abstract val valueNodes: MutableList<ValueNode<*>> private fun findNodeInstance(name: String): ValueNode<*>? {
check(this is AbstractPluginData)
return valueNodes.firstOrNull { it.valueName == name }
}
internal open val updaterSerializer: KSerializer<Unit> = object : KSerializer<Unit> { internal open val updaterSerializer: KSerializer<Unit> = object : KSerializer<Unit> {
override val descriptor: SerialDescriptor get() = dataUpdaterSerializerDescriptor override val descriptor: SerialDescriptor by lazy {
check(this@PluginDataImpl is AbstractPluginData)
kotlinx.serialization.descriptors.buildClassSerialDescriptor((this@PluginDataImpl as PluginData).saveName) {
for (valueNode in valueNodes) valueNode.run {
element(valueName, updaterSerializer.descriptor, annotations = annotations, isOptional = true)
}
}
}
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
override fun deserialize(decoder: Decoder) { override fun deserialize(decoder: Decoder) {
@ -84,6 +97,8 @@ internal abstract class PluginDataImpl {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
override fun serialize(encoder: Encoder, value: Unit) { override fun serialize(encoder: Encoder, value: Unit) {
check(this@PluginDataImpl is AbstractPluginData)
val descriptor = descriptor val descriptor = descriptor
with(encoder.beginStructure(descriptor)) { with(encoder.beginStructure(descriptor)) {
repeat(descriptor.elementsCount) { index -> repeat(descriptor.elementsCount) { index ->
@ -100,18 +115,6 @@ internal abstract class PluginDataImpl {
} }
} }
/**
* flatten
*/
abstract fun onValueChanged(value: Value<*>)
private val dataUpdaterSerializerDescriptor by lazy {
kotlinx.serialization.descriptors.buildClassSerialDescriptor((this as PluginData).saveName) {
for (valueNode in valueNodes) valueNode.run {
element(valueName, updaterSerializer.descriptor, annotations = annotations, isOptional = true)
}
}
}
} }
internal fun KAnnotatedElement.getAnnotationListForValueSerialization(): List<Annotation> { internal fun KAnnotatedElement.getAnnotationListForValueSerialization(): List<Annotation> {

View File

@ -6,10 +6,7 @@ import net.mamoe.mirai.console.data.value
import net.mamoe.mirai.console.internal.util.md5 import net.mamoe.mirai.console.internal.util.md5
import net.mamoe.mirai.console.internal.util.toUHexString import net.mamoe.mirai.console.internal.util.toUHexString
internal object AutoLoginConfig : AutoSavePluginConfig() { internal object AutoLoginConfig : AutoSavePluginConfig("AutoLogin") {
override val saveName: String
get() = "AutoLogin"
@ValueDescription( @ValueDescription(
""" """
账号和明文密码列表 账号和明文密码列表

View File

@ -143,7 +143,7 @@ internal fun KClass<*>.isPrimitiveOrBuiltInSerializableValue(): Boolean {
Byte::class, Short::class, Int::class, Long::class, Byte::class, Short::class, Int::class, Long::class,
Boolean::class, Boolean::class,
Char::class, String::class, Char::class, String::class,
Pair::class, Triple::class // TODO: 2020/6/24 支持 PairValue, TripleValue //Pair::class, Triple::class // TODO: 2020/6/24 支持 PairValue, TripleValue
-> return true -> return true
} }

View File

@ -16,9 +16,7 @@ internal object BuiltInSingletonExtensionSelector : SingletonExtensionSelector {
internal val config: SaveData = SaveData() internal val config: SaveData = SaveData()
internal class SaveData : AutoSavePluginConfig() { internal class SaveData : AutoSavePluginConfig("ExtensionSelector") {
override val saveName: String get() = "ExtensionSelector"
val value: MutableMap<String, String> by value() val value: MutableMap<String, String> by value()
} }

View File

@ -117,12 +117,12 @@ internal object BuiltInPermissionService : AbstractConcurrentPermissionService<P
@Suppress("RedundantVisibilityModifier") @Suppress("RedundantVisibilityModifier")
internal class ConcurrentSaveData private constructor( internal class ConcurrentSaveData private constructor(
public override val saveName: String, saveName: String,
@Suppress("UNUSED_PARAMETER") primaryConstructorMark: Any?, @Suppress("UNUSED_PARAMETER") primaryConstructorMark: Any?,
) : AutoSavePluginConfig() { ) : AutoSavePluginConfig(saveName) {
public val grantedPermissionMap: PluginDataExtensions.NotNullMutableMap<PermissionId, MutableSet<AbstractPermitteeId>> public val grantedPermissionMap: PluginDataExtensions.NotNullMutableMap<PermissionId, MutableSet<AbstractPermitteeId>>
by value<MutableMap<PermissionId, MutableSet<AbstractPermitteeId>>>(ConcurrentHashMap()) by value<MutableMap<PermissionId, MutableSet<AbstractPermitteeId>>>(ConcurrentHashMap())
.withDefault { CopyOnWriteArraySet() } .withDefault { CopyOnWriteArraySet() }
public companion object { public companion object {
@JvmStatic @JvmStatic

View File

@ -68,12 +68,12 @@ internal object BuiltInJvmPluginLoaderImpl :
pluginClassLoader.declaredFilter = exportManagers[0] pluginClassLoader.declaredFilter = exportManagers[0]
} }
}.map { (f, pluginClassLoader) -> }.map { (f, pluginClassLoader) ->
f to (pluginClassLoader.findServices( f to pluginClassLoader.findServices(
JvmPlugin::class, JvmPlugin::class,
KotlinPlugin::class, KotlinPlugin::class,
AbstractJvmPlugin::class, AbstractJvmPlugin::class,
JavaPlugin::class JavaPlugin::class
).loadAllServices()) ).loadAllServices()
}.flatMap { (f, list) -> }.flatMap { (f, list) ->
list.associateBy { f }.asSequence() list.associateBy { f }.asSequence()
@ -121,7 +121,9 @@ internal object BuiltInJvmPluginLoaderImpl :
override fun disable(plugin: JvmPlugin) { override fun disable(plugin: JvmPlugin) {
if (!plugin.isEnabled) return if (!plugin.isEnabled) return
ensureActive()
if (MiraiConsole.isActive)
ensureActive()
if (plugin is JvmPluginInternal) { if (plugin is JvmPluginInternal) {
plugin.internalOnDisable() plugin.internalOnDisable()

View File

@ -7,24 +7,28 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * https://github.com/mamoe/mirai/blob/master/LICENSE
* *
*/ */
@file:Suppress("MemberVisibilityCanBePrivate") @file:Suppress("MemberVisibilityCanBePrivate")
package net.mamoe.mirai.console.internal.plugin package net.mamoe.mirai.console.internal.plugin
import net.mamoe.mirai.console.plugin.jvm.ExportManager import net.mamoe.mirai.console.plugin.jvm.ExportManager
import java.io.File
import java.net.URL import java.net.URL
import java.net.URLClassLoader import java.net.URLClassLoader
import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
internal class LoadingDeniedException(name: String) : ClassNotFoundException(name)
internal class JvmPluginClassLoader( internal class JvmPluginClassLoader(
val source: Any, val file: File,
urls: Array<URL>,
parent: ClassLoader?, parent: ClassLoader?,
val classLoaders: Collection<JvmPluginClassLoader> val classLoaders: Collection<JvmPluginClassLoader>,
) : URLClassLoader(urls, parent) { ) : URLClassLoader(arrayOf(file.toURI().toURL()), parent) {
//// 只允许插件 getResource 时获取插件自身资源, #205
override fun getResources(name: String?): Enumeration<URL> = findResources(name)
override fun getResource(name: String?): URL? = findResource(name)
// getResourceAsStream 在 URLClassLoader 中通过 getResource 确定资源
// 因此无需 override getResourceAsStream
override fun toString(): String { override fun toString(): String {
return "JvmPluginClassLoader{source=$source}" return "JvmPluginClassLoader{source=$source}"
} }
@ -118,3 +122,5 @@ internal class JvmPluginClassLoader(
} }
} }
} }
internal class LoadingDeniedException(name: String) : ClassNotFoundException(name)

View File

@ -18,7 +18,6 @@ import net.mamoe.mirai.console.extension.PluginComponentStorage
import net.mamoe.mirai.console.internal.data.mkdir import net.mamoe.mirai.console.internal.data.mkdir
import net.mamoe.mirai.console.permission.Permission import net.mamoe.mirai.console.permission.Permission
import net.mamoe.mirai.console.permission.PermissionService import net.mamoe.mirai.console.permission.PermissionService
import net.mamoe.mirai.console.permission.PermissionService.Companion.allocatePermissionIdForPlugin
import net.mamoe.mirai.console.plugin.Plugin import net.mamoe.mirai.console.plugin.Plugin
import net.mamoe.mirai.console.plugin.PluginManager import net.mamoe.mirai.console.plugin.PluginManager
import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.safeLoader import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.safeLoader
@ -26,7 +25,6 @@ import net.mamoe.mirai.console.plugin.ResourceContainer.Companion.asResourceCont
import net.mamoe.mirai.console.plugin.jvm.AbstractJvmPlugin import net.mamoe.mirai.console.plugin.jvm.AbstractJvmPlugin
import net.mamoe.mirai.console.plugin.jvm.JvmPlugin import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
import net.mamoe.mirai.console.plugin.jvm.JvmPlugin.Companion.onLoad import net.mamoe.mirai.console.plugin.jvm.JvmPlugin.Companion.onLoad
import net.mamoe.mirai.console.plugin.name
import net.mamoe.mirai.console.util.NamedSupervisorJob import net.mamoe.mirai.console.util.NamedSupervisorJob
import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.MiraiLogger
import java.io.File import java.io.File
@ -50,7 +48,7 @@ internal abstract class JvmPluginInternal(
final override val parentPermission: Permission by lazy { final override val parentPermission: Permission by lazy {
PermissionService.INSTANCE.register( PermissionService.INSTANCE.register(
PermissionService.INSTANCE.allocatePermissionIdForPlugin(name, "*"), PermissionService.INSTANCE.allocatePermissionIdForPlugin(this, "*", PermissionService.PluginPermissionIdRequestType.ROOT_PERMISSION),
"The base permission" "The base permission"
) )
} }
@ -151,6 +149,7 @@ internal abstract class JvmPluginInternal(
) )
) )
.also { .also {
if (!MiraiConsole.isActive) return@also
BuiltInJvmPluginLoaderImpl.coroutineContext[Job]!!.invokeOnCompletion { BuiltInJvmPluginLoaderImpl.coroutineContext[Job]!!.invokeOnCompletion {
this.cancel() this.cancel()
} }

View File

@ -27,6 +27,7 @@ import net.mamoe.mirai.console.plugin.loader.PluginLoadException
import net.mamoe.mirai.console.plugin.loader.PluginLoader import net.mamoe.mirai.console.plugin.loader.PluginLoader
import net.mamoe.mirai.console.plugin.name import net.mamoe.mirai.console.plugin.name
import net.mamoe.mirai.console.util.CoroutineScopeUtils.childScope import net.mamoe.mirai.console.util.CoroutineScopeUtils.childScope
import net.mamoe.mirai.console.util.SemVersion.Companion.contains
import net.mamoe.mirai.utils.info import net.mamoe.mirai.utils.info
import java.io.File import java.io.File
import java.nio.file.Path import java.nio.file.Path

View File

@ -0,0 +1,251 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*
*/
@file:Suppress("MemberVisibilityCanBePrivate")
package net.mamoe.mirai.console.internal.util.semver
import net.mamoe.mirai.console.util.SemVersion
import kotlin.math.max
import kotlin.math.min
internal object RangeTokenReader {
enum class TokenType {
STRING,
/* 左括号 */
LEFT,
/* 右括号 */
RIGHT,
/* || */
OR,
/* && */
AND,
GROUP
}
sealed class Token {
abstract val type: TokenType
abstract val value: String
abstract val position: Int
class LeftBracket(override val position: Int) : Token() {
override val type: TokenType get() = TokenType.LEFT
override val value: String get() = "{"
override fun toString(): String = "LB{"
}
class RightBracket(override val position: Int) : Token() {
override val type: TokenType get() = TokenType.RIGHT
override val value: String get() = "}"
override fun toString(): String = "RB}"
}
class Or(override val position: Int) : Token() {
override val type: TokenType get() = TokenType.OR
override val value: String get() = "||"
override fun toString(): String = "OR||"
}
class And(override val position: Int) : Token() {
override val type: TokenType get() = TokenType.AND
override val value: String get() = "&&"
override fun toString(): String = "AD&&"
}
class Group(val values: List<Token>, override val position: Int) : Token() {
override val type: TokenType get() = TokenType.GROUP
override val value: String get() = ""
}
class Raw(val source: String, val start: Int, val end: Int) : Token() {
override val value: String get() = source.substring(start, end)
override val position: Int
get() = start
override val type: TokenType get() = TokenType.STRING
override fun toString(): String = "R:$value"
}
}
fun parseToTokens(source: String): List<Token> = ArrayList<Token>(
max(source.length / 3, 16)
).apply {
var index = 0
var position = 0
fun flushOld() {
if (position > index) {
val id = index
index = position
for (i in id until position) {
if (!source[i].isWhitespace()) {
add(Token.Raw(source, id, position))
return
}
}
}
}
val iterator = source.indices.iterator()
for (i in iterator) {
position = i
when (source[i]) {
'{' -> {
flushOld()
add(Token.LeftBracket(i))
index = i + 1
}
'|' -> {
if (source.getOrNull(i + 1) == '|') {
flushOld()
add(Token.Or(i))
index = i + 2
iterator.nextInt()
}
}
'&' -> {
if (source.getOrNull(i + 1) == '&') {
flushOld()
add(Token.And(i))
index = i + 2
iterator.nextInt()
}
}
'}' -> {
flushOld()
add(Token.RightBracket(i))
index = i + 1
}
}
}
position = source.length
flushOld()
}
fun collect(source: String, tokens: Iterator<Token>, root: Boolean): List<Token> = ArrayList<Token>().apply {
tokens.forEach { token ->
if (token is Token.LeftBracket) {
add(Token.Group(collect(source, tokens, false), token.position))
} else if (token is Token.RightBracket) {
if (root) {
throw IllegalArgumentException("Syntax error: Unexpected }, ${buildMsg(source, token.position)}")
} else {
return@apply
}
} else add(token)
}
if (!root) {
throw IllegalArgumentException("Syntax error: Excepted }, ${buildMsg(source, source.length)}")
}
}
private fun buildMsg(source: String, position: Int): String {
val ed = min(position + 10, source.length)
val st = max(0, position - 10)
return buildString {
append('`')
if (st != 0) append("...")
append(source, st, ed)
if (ed != source.length) append("...")
append("` at ").append(position)
}
}
fun check(source: String, tokens: Iterator<Token>, group: Token.Group?) {
if (!tokens.hasNext()) {
throw IllegalArgumentException("Syntax error: empty rule, ${buildMsg(source, group?.position ?: 0)}")
}
var type = false
do {
val next = tokens.next()
if (type) {
if (next is Token.Group || next is Token.Raw) {
throw IllegalArgumentException("Syntax error: Except logic but got expression, ${buildMsg(source, next.position)}")
}
} else {
if (next is Token.Or || next is Token.And) {
throw IllegalArgumentException("Syntax error: Except expression but got logic, ${buildMsg(source, next.position)}")
}
if (next is Token.Group) {
check(source, next.values.iterator(), next)
}
}
type = !type
} while (tokens.hasNext())
if (!type) {
throw IllegalArgumentException("Syntax error: Except more expression, ${buildMsg(source, group?.values?.last()?.position ?: source.length)}")
}
}
fun parse(source: String, token: Token): SemVersion.Requirement {
return when (token) {
is Token.Group -> {
if (token.values.size == 1) {
parse(source, token.values.first())
} else {
val logic = token.values.asSequence().map { it.type }.filter {
it == TokenType.OR || it == TokenType.AND
}.toSet()
if (logic.size == 2) {
throw IllegalArgumentException("Syntax error: || and && cannot use in one group, ${buildMsg(source, token.position)}")
}
val rules = token.values.asSequence().filter {
it is Token.Raw || it is Token.Group
}.map { parse(source, it) }.toList()
when (logic.first()) {
TokenType.OR -> {
return object : SemVersion.Requirement {
override fun test(version: SemVersion): Boolean {
rules.forEach { if (it.test(version)) return true }
return false
}
}
}
TokenType.AND -> {
return object : SemVersion.Requirement {
override fun test(version: SemVersion): Boolean {
rules.forEach { if (!it.test(version)) return false }
return true
}
}
}
else -> throw AssertionError()
}
}
}
is Token.Raw -> SemVersionInternal.parseRule(token.value)
else -> throw AssertionError()
}
}
fun StringBuilder.dump(prefix: String, token: Token) {
when (token) {
is Token.LeftBracket -> append("${prefix}LF {\n")
is Token.RightBracket -> append("${prefix}LR }\n")
is Token.Or -> append("${prefix}OR ||\n")
is Token.And -> append("${prefix}AND &&\n")
is Token.Group -> {
append("${prefix}GROUP {\n")
token.values.forEach { dump("$prefix ", it) }
append("${prefix}}\n")
}
is Token.Raw -> append("${prefix}RAW ${token.value}\n")
}
}
}

View File

@ -0,0 +1,290 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*
*/
package net.mamoe.mirai.console.internal.util.semver
import net.mamoe.mirai.console.internal.util.semver.RangeTokenReader.dump
import net.mamoe.mirai.console.util.SemVersion
import kotlin.math.max
import kotlin.math.min
@Suppress("RegExpRedundantEscape")
internal object SemVersionInternal {
private val directVersion = """^[0-9]+(\.[0-9]+)+(|[\-+].+)$""".toRegex()
private val versionSelect = """^[0-9]+(\.[0-9]+)*\.x$""".toRegex()
private val versionMathRange =
"""([\[\(])([0-9]+(\.[0-9]+)+(|[\-+].+))\s*\,\s*([0-9]+(\.[0-9]+)+(|[\-+].+))([\]\)])""".toRegex()
private val versionRule = """^((\>\=)|(\<\=)|(\=)|(\!\=)|(\>)|(\<))\s*([0-9]+(\.[0-9]+)+(|[\-+].+))$""".toRegex()
private val SEM_VERSION_REGEX =
"""^(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$""".toRegex()
/** 解析核心版本号, eg: `1.0.0` -> IntArray[1, 0, 0] */
@JvmStatic
private fun String.parseMainVersion(): IntArray =
split('.').map { it.toInt() }.toIntArray()
fun parse(version: String): SemVersion {
if (!SEM_VERSION_REGEX.matches(version)) {
throw IllegalArgumentException("`$version` not a valid version")
}
var mainVersionEnd = 0
kotlin.run {
val iterator = version.iterator()
while (iterator.hasNext()) {
val next = iterator.next()
if (next == '-' || next == '+') {
break
}
mainVersionEnd++
}
}
var identifier: String? = null
var metadata: String? = null
if (mainVersionEnd != version.length) {
when (version[mainVersionEnd]) {
'-' -> {
val metadataSplitter = version.indexOf('+', startIndex = mainVersionEnd)
if (metadataSplitter == -1) {
identifier = version.substring(mainVersionEnd + 1)
} else {
identifier = version.substring(mainVersionEnd + 1, metadataSplitter)
metadata = version.substring(metadataSplitter + 1)
}
}
'+' -> {
metadata = version.substring(mainVersionEnd + 1)
}
}
}
val mainVersion = version.substring(0, mainVersionEnd).parseMainVersion()
return SemVersion(
major = mainVersion[0],
minor = mainVersion[1],
patch = mainVersion.getOrNull(2),
identifier = identifier,
metadata = metadata
)
}
@JvmStatic
internal fun parseRule(rule: String): SemVersion.Requirement {
val trimmed = rule.trim()
if (directVersion.matches(trimmed)) {
val parsed = SemVersion.invoke(trimmed)
return object : SemVersion.Requirement {
override fun test(version: SemVersion): Boolean = version.compareTo(parsed) == 0
}
}
if (versionSelect.matches(trimmed)) {
val regex = ("^" +
trimmed.replace(".", "\\.")
.replace("x", ".+") +
"$"
).toRegex()
return object : SemVersion.Requirement {
override fun test(version: SemVersion): Boolean = regex.matches(version.toString())
}
}
versionMathRange.matchEntire(trimmed)?.let { range ->
// 1 mode
// 2 first
// 5 sec
// 8 type
var typeStart = range.groupValues[1][0]
var typeEnd = range.groupValues[8][0]
var start = SemVersion.invoke(range.groupValues[2])
var end = SemVersion.invoke(range.groupValues[5])
if (start > end) {
val c = end
end = start
start = c
val x = typeEnd
typeEnd = typeStart
typeStart = x
}
val a: (SemVersion) -> Boolean = when (typeStart) {
'[', ']' -> ({ start <= it })
'(', ')' -> ({ start < it })
else -> throw AssertionError()
}
val b: (SemVersion) -> Boolean = when (typeEnd) {
'[', ']' -> ({ it <= end })
'(', ')' -> ({ it < end })
else -> throw AssertionError()
}
return object : SemVersion.Requirement {
override fun test(version: SemVersion): Boolean = a(version) && b(version)
}
}
versionRule.matchEntire(trimmed)?.let { result ->
val operator = result.groupValues[1]
val version1 = SemVersion.invoke(result.groupValues[8])
return when (operator) {
">=" -> {
object : SemVersion.Requirement {
override fun test(version: SemVersion): Boolean = version >= version1
}
}
">" -> {
object : SemVersion.Requirement {
override fun test(version: SemVersion): Boolean = version > version1
}
}
"<=" -> {
object : SemVersion.Requirement {
override fun test(version: SemVersion): Boolean = version <= version1
}
}
"<" -> {
object : SemVersion.Requirement {
override fun test(version: SemVersion): Boolean = version < version1
}
}
"=" -> {
object : SemVersion.Requirement {
override fun test(version: SemVersion): Boolean = version.compareTo(version1) == 0
}
}
"!=" -> {
object : SemVersion.Requirement {
override fun test(version: SemVersion): Boolean = version.compareTo(version1) != 0
}
}
else -> error("operator=$operator, version=$version1")
}
}
throw IllegalArgumentException("Cannot parse $rule")
}
private fun SemVersion.Requirement.withRule(rule: String): SemVersion.Requirement {
return object : SemVersion.Requirement {
override fun test(version: SemVersion): Boolean = this@withRule.test(version)
override fun toString(): String = rule
}
}
@JvmStatic
fun parseRangeRequirement(requirement: String): SemVersion.Requirement {
if (requirement.isBlank()) {
throw IllegalArgumentException("Invalid requirement: Empty requirement rule.")
}
val tokens = RangeTokenReader.parseToTokens(requirement)
val collected = RangeTokenReader.collect(requirement, tokens.iterator(), true)
RangeTokenReader.check(requirement, collected.iterator(), null)
return kotlin.runCatching {
RangeTokenReader.parse(requirement, RangeTokenReader.Token.Group(collected, 0)).withRule(requirement)
}.onFailure { error ->
throw IllegalArgumentException("Exception in parsing $requirement\n\n" + buildString {
collected.forEach { dump("", it) }
}, error)
}.getOrThrow()
}
@JvmStatic
fun compareInternal(source: SemVersion, other: SemVersion): Int {
// ignored metadata in comparing
// If $this equals $other (without metadata),
// return same.
// Compare main-version
source.major.compareTo(other.major).takeUnless { it == 0 }?.let { return it }
source.minor.compareTo(other.minor).takeUnless { it == 0 }?.let { return it }
(source.patch ?: 0).compareTo(other.patch ?: 0).takeUnless { it == 0 }?.let { return it }
// If main-versions are same.
var identifier0 = source.identifier
var identifier1 = other.identifier
// If anyone doesn't have the identifier...
if (identifier0 == null || identifier1 == null) {
return when (identifier0) {
identifier1 -> { // null == null
// Nobody has identifier
0
}
null -> {
// $other has identifier, but $this don't have identifier
// E.g:
// this = 1.0.0
// other = 1.0.0-dev
1
}
// It is the opposite of the above.
else -> -1
}
}
fun String.getSafe(index: Int) = getOrElse(index) { ' ' }
// ignored same prefix
fun getSameSize(s1: String, s2: String): Int {
val size = min(s1.length, s2.length)
// 1.0-RC19 -> 19
// 1.0-RC107 -> 107
var realSameSize = 0
for (index in 0 until size) {
if (s1[index] != s2[index]) {
return realSameSize
} else {
if (!s1[index].isDigit()) {
realSameSize = index + 1
}
}
}
return realSameSize
}
// We ignore the same parts. Because we only care about the differences.
// E.g:
// 1.0-RC1 -> 1
// 1.0-RC2 -> 2
val ignoredSize = getSameSize(identifier0, identifier1)
identifier0 = identifier0.substring(ignoredSize)
identifier1 = identifier1.substring(ignoredSize)
// Multi-chunk comparing
val chunks0 = identifier0.split('-', '.')
val chunks1 = identifier1.split('-', '.')
chunkLoop@ for (index in 0 until (max(chunks0.size, chunks1.size))) {
val value0 = chunks0.getOrNull(index)
val value1 = chunks1.getOrNull(index)
// Any chunk is null
if (value0 == null || value1 == null) {
// value0 == null && value1 == null is impossible
return if (value0 == null) {
// E.g:
// value0 = 1.0-RC-dev
// value1 = 1.0-RC-dev-1
-1
} else {
// E.g:
// value0 = 1.0-RC-dev-1
// value1 = 1.0-RC-dev
1
}
}
try {
val result = value0.toInt().compareTo(value1.toInt())
if (result != 0) {
return result
}
continue@chunkLoop
} catch (ignored: NumberFormatException) {
}
// compare chars
for (index0 in 0 until (max(value0.length, value1.length))) {
val result = value0.getSafe(index0).compareTo(value1.getSafe(index0))
if (result != 0)
return result
}
}
return 0
}
}

View File

@ -13,9 +13,9 @@ import net.mamoe.mirai.console.command.BuiltInCommands
import net.mamoe.mirai.console.command.Command import net.mamoe.mirai.console.command.Command
/** /**
* 一个权限. * 一个抽象的权限. [PermissionService] 实现不同, [Permission] 可能会有多种实例. 但一个权限总是拥有确定的 [id].
* *
* [PermissionService] 实现不同, [Permission] 可能会有多种实例. 但一个权限总是拥有确定的 [id]. * 在匹配权限时, 应使用唯一的 [id] 作为依据. 而不应该使用 [Permission] 实例. 同时, [Permission] 也不适合存储.
* *
* **注意**: 请不要手动实现这个接口. 总是从 [PermissionService.register] 获得实例. * **注意**: 请不要手动实现这个接口. 总是从 [PermissionService.register] 获得实例.
* *
@ -32,6 +32,7 @@ import net.mamoe.mirai.console.command.Command
* #### 手动申请权限 * #### 手动申请权限
* [PermissionService.register] * [PermissionService.register]
*/ */
@PermissionImplementation
public interface Permission { public interface Permission {
/** /**
* 唯一识别 ID. 所有权限的 [id] 都互不相同. * 唯一识别 ID. 所有权限的 [id] 都互不相同.
@ -49,6 +50,8 @@ public interface Permission {
/** /**
* 父权限. * 父权限.
* *
* 在检查权限时, 若一个 [Permittee] 拥有父
*
* [RootPermission] parent 为自身 * [RootPermission] parent 为自身
*/ */
public val parent: Permission public val parent: Permission

View File

@ -12,6 +12,8 @@ package net.mamoe.mirai.console.permission
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.serializer import kotlinx.serialization.builtins.serializer
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.*
import net.mamoe.mirai.console.internal.data.map import net.mamoe.mirai.console.internal.data.map
@ -25,10 +27,17 @@ import net.mamoe.mirai.console.internal.data.map
*/ */
@Serializable(with = PermissionId.PermissionIdAsStringSerializer::class) @Serializable(with = PermissionId.PermissionIdAsStringSerializer::class)
public data class PermissionId( public data class PermissionId(
public val namespace: String, @ResolveContext(PERMISSION_NAMESPACE) public val namespace: String,
public val name: String, @ResolveContext(PERMISSION_NAME) public val name: String,
) { ) {
init { init {
require(!namespace.contains(' ')) {
"' ' is not allowed in namespace"
}
require(!name.contains(' ')) {
"' ' is not allowed in id"
}
require(!namespace.contains(':')) { require(!namespace.contains(':')) {
"':' is not allowed in namespace" "':' is not allowed in namespace"
} }
@ -54,13 +63,39 @@ public data class PermissionId(
* @throws IllegalArgumentException 在解析失败时抛出. * @throws IllegalArgumentException 在解析失败时抛出.
*/ */
@JvmStatic @JvmStatic
public fun parseFromString(string: String): PermissionId { public fun parseFromString(@ResolveContext(PERMISSION_ID) string: String): PermissionId {
return kotlin.runCatching { return kotlin.runCatching {
string.split(':').let { (namespace, id) -> PermissionId(namespace, id) } string.split(':').let { (namespace, id) -> PermissionId(namespace, id) }
}.getOrElse { }.getOrElse {
throw IllegalArgumentException("Could not parse PermissionId from '$string'", it) throw IllegalArgumentException("Could not parse PermissionId from '$string'", it)
} }
} }
/**
* 检查 [PermissionId.name] 的合法性. 在非法时抛出 [IllegalArgumentException]
*/
@JvmStatic
@Throws(IllegalArgumentException::class)
public fun checkPermissionIdName(@ResolveContext(PERMISSION_NAME) value: String) {
when {
value.isBlank() -> throw IllegalArgumentException("PermissionId.name should not be blank.")
value.any { it.isWhitespace() } -> throw IllegalArgumentException("Spaces is not yet allowed in PermissionId.name.")
value.contains(':') -> throw IllegalArgumentException("':' is forbidden in PermissionId.name.")
}
}
/**
* 检查 [PermissionId.namespace] 的合法性. 在非法时抛出 [IllegalArgumentException]
*/
@JvmStatic
@Throws(IllegalArgumentException::class)
public fun checkPermissionIdNamespace(@ResolveContext(PERMISSION_NAME) value: String) {
when {
value.isBlank() -> throw IllegalArgumentException("PermissionId.namespace should not be blank.")
value.any { it.isWhitespace() } -> throw IllegalArgumentException("Spaces is not yet allowed in PermissionId.namespace.")
value.contains(':') -> throw IllegalArgumentException("':' is forbidden in PermissionId.namespace.")
}
}
} }
} }

View File

@ -9,12 +9,18 @@
package net.mamoe.mirai.console.permission package net.mamoe.mirai.console.permission
import net.mamoe.mirai.console.command.Command
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.PERMISSION_NAME
/** /**
* [PermissionId] 的命名空间. 用于提供 [PermissionId.namespace]. * [PermissionId] 的命名空间. 用于提供 [PermissionId.namespace].
*/ */
public interface PermissionIdNamespace { public interface PermissionIdNamespace {
/** /**
* 创建一个此命名空间下的 [PermitteeId] * 创建一个此命名空间下的 [PermitteeId].
*
* 在指令初始化时, 会申请对应权限. 此时 [name] "command.$primaryName` 其中 [primaryName][Command.primaryName].
*/ */
public fun permissionId(name: String): PermissionId public fun permissionId(@ResolveContext(PERMISSION_NAME) name: String): PermissionId
} }

View File

@ -12,10 +12,12 @@ package net.mamoe.mirai.console.permission
import kotlin.annotation.AnnotationTarget.* import kotlin.annotation.AnnotationTarget.*
/** /**
* 表示一个应该由权限插件实现的类. * 表示一个应该由专有的权限插件 (提供 [PermissionService] 的插件) 实现的类.
* *
* 这样的类不能被用户手动实现或者继承, 也不能使用属性委托或者类委托, 或者其他任意改变实现类的手段. *
* 用户仅应该使用从 [PermissionService] 或其他途径获取这些对象, 而不能自行实现它们. * 这样的类不能被用户手动实现或者继承, 也不能使用属性委托或者类委托, 或者其他任意直接或间接实现他们的手段 (否则会导致 [PermissionService] 处理异常).
*
* 普通插件仅应该使用从 [PermissionService] 或其他途径获取这些对象.
*/ */
@Retention(AnnotationRetention.BINARY) @Retention(AnnotationRetention.BINARY)
@Target(CLASS, TYPEALIAS, FUNCTION, PROPERTY, FIELD, CONSTRUCTOR) @Target(CLASS, TYPEALIAS, FUNCTION, PROPERTY, FIELD, CONSTRUCTOR)

View File

@ -11,9 +11,13 @@
package net.mamoe.mirai.console.permission package net.mamoe.mirai.console.permission
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.COMMAND_NAME
import net.mamoe.mirai.console.extensions.PermissionServiceProvider import net.mamoe.mirai.console.extensions.PermissionServiceProvider
import net.mamoe.mirai.console.internal.permission.checkType import net.mamoe.mirai.console.internal.permission.checkType
import net.mamoe.mirai.console.permission.Permission.Companion.parentsWithSelf import net.mamoe.mirai.console.permission.Permission.Companion.parentsWithSelf
import net.mamoe.mirai.console.plugin.Plugin
import net.mamoe.mirai.console.plugin.name
import kotlin.reflect.KClass import kotlin.reflect.KClass
/** /**
@ -77,6 +81,8 @@ public interface PermissionService<P : Permission> {
* 申请并注册一个权限 [Permission]. * 申请并注册一个权限 [Permission].
* *
* @throws PermissionRegistryConflictException 当已存在一个 [PermissionId] 时抛出. * @throws PermissionRegistryConflictException 当已存在一个 [PermissionId] 时抛出.
*
* @return 申请到的 [Permission] 实例
*/ */
@Throws(PermissionRegistryConflictException::class) @Throws(PermissionRegistryConflictException::class)
public fun register( public fun register(
@ -85,21 +91,29 @@ public interface PermissionService<P : Permission> {
parent: Permission = RootPermission, parent: Permission = RootPermission,
): P ): P
/** 为 [Plugin] 分配一个 [PermissionId] */
public fun allocatePermissionIdForPlugin(
plugin: Plugin,
@ResolveContext(COMMAND_NAME) permissionName: String,
reason: PluginPermissionIdRequestType
): PermissionId = allocatePermissionIdForPluginDefaultImplement(plugin, permissionName, reason)
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
/** /**
* 授予 [permitteeId] [permission] 权限 * 授予 [permitteeId] [permission] 权限
* *
* Console 内建的权限服务支持授予操作. 但插件扩展的权限服务可能不支持. * Console 内建的权限服务支持操作. 但插件扩展的权限服务可能不支持.
* *
* @throws UnsupportedOperationException 当插件扩展的 [PermissionService] 不支持这样的操作时抛出. * @throws UnsupportedOperationException 当插件扩展的 [PermissionService] 不支持这样的操作时抛出.
*/ */
@Throws(UnsupportedOperationException::class)
public fun permit(permitteeId: PermitteeId, permission: P) public fun permit(permitteeId: PermitteeId, permission: P)
/** /**
* 撤销 [permitteeId] [permission] 授权 * 撤销 [permitteeId] [permission] 授权
* *
* Console 内建的权限服务支持授予操作. 但插件扩展的权限服务可能不支持. * Console 内建的权限服务支持操作. 但插件扩展的权限服务可能不支持.
* *
* @param recursive `true` 时递归撤销所有子权限. * @param recursive `true` 时递归撤销所有子权限.
* 例如, [permission] "*:*", * 例如, [permission] "*:*",
@ -108,8 +122,18 @@ public interface PermissionService<P : Permission> {
* *
* @throws UnsupportedOperationException 当插件扩展的 [PermissionService] 不支持这样的操作时抛出. * @throws UnsupportedOperationException 当插件扩展的 [PermissionService] 不支持这样的操作时抛出.
*/ */
@Throws(UnsupportedOperationException::class)
public fun cancel(permitteeId: PermitteeId, permission: P, recursive: Boolean) public fun cancel(permitteeId: PermitteeId, permission: P, recursive: Boolean)
/** [Plugin] 尝试分配的 [PermissionId] 来源 */
public enum class PluginPermissionIdRequestType {
/** For [Plugin.parentPermission] */
ROOT_PERMISSION,
/** For [Plugin.permissionId] */
PERMISSION_ID
}
public companion object { public companion object {
internal var instanceField: PermissionService<*>? = null internal var instanceField: PermissionService<*>? = null
@ -118,11 +142,21 @@ public interface PermissionService<P : Permission> {
public val INSTANCE: PermissionService<out Permission> public val INSTANCE: PermissionService<out Permission>
get() = instanceField ?: error("PermissionService is not yet initialized therefore cannot be used.") get() = instanceField ?: error("PermissionService is not yet initialized therefore cannot be used.")
/**
* 获取一个权限, 失败时抛出 [NoSuchElementException]
*/
@Throws(NoSuchElementException::class)
public fun <P : Permission> PermissionService<P>.getOrFail(id: PermissionId): P = public fun <P : Permission> PermissionService<P>.getOrFail(id: PermissionId): P =
get(id) ?: throw NoSuchElementException("Permission not found: $id") get(id) ?: throw NoSuchElementException("Permission not found: $id")
internal fun PermissionService<*>.allocatePermissionIdForPlugin(name: String, id: String) = internal fun PermissionService<*>.allocatePermissionIdForPluginDefaultImplement(
PermissionId("plugin.${name.toLowerCase()}", id.toLowerCase()) plugin: Plugin,
@ResolveContext(COMMAND_NAME) permissionName: String,
reason: PluginPermissionIdRequestType
) = PermissionId(
plugin.name.toLowerCase().replace(' ', '.'),
permissionName.toLowerCase().replace(' ', '.')
)
public fun PermissionId.findCorrespondingPermission(): Permission? = INSTANCE[this] public fun PermissionId.findCorrespondingPermission(): Permission? = INSTANCE[this]

View File

@ -14,6 +14,7 @@ package net.mamoe.mirai.console.permission
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.serializer import kotlinx.serialization.builtins.serializer
import net.mamoe.mirai.console.command.BuiltInCommands
import net.mamoe.mirai.console.internal.data.map import net.mamoe.mirai.console.internal.data.map
import net.mamoe.mirai.console.internal.permission.parseFromStringImpl import net.mamoe.mirai.console.internal.permission.parseFromStringImpl
import net.mamoe.mirai.console.permission.AbstractPermitteeId.* import net.mamoe.mirai.console.permission.AbstractPermitteeId.*
@ -107,6 +108,33 @@ public interface PermitteeId {
* *
* - 若指令 A 的权限被授予给 [AnyMember], 那么一个 [ExactMember] 可以执行这个指令. * - 若指令 A 的权限被授予给 [AnyMember], 那么一个 [ExactMember] 可以执行这个指令.
* *
* #### 字符串表示
*
* 当使用 [PermitteeId.asString] , 不同的类型的返回值如下表所示. 这些格式也适用于 [BuiltInCommands.PermissionCommand].
*
* (不区分大小写. 不区分 Bot).
*
*
* | 被许可人类型 | 字符串表示示例 | 备注 |
* |:----------------:|:-----------:|:-------------------------------------|
* | 控制台 | console | |
* | 精确群 | g123456 | 表示群, 而不表示群成员 |
* | 精确好友 | f123456 | 必须通过好友消息 |
* | 精确临时会话 | t123456.789 | 123456 内的成员 789. 必须通过临时会话 |
* | 精确群成员 | m123456.789 | 123456 内的成员 789. 同时包含临时会话. |
* | 精确用户 | u123456 | 同时包含群成员, 好友, 临时会话 |
* | 任意群 | g* | |
* | 任意群的任意群员 | m* | |
* | 精确群的任意群员 | m123456.* | 123456 内的任意成员. 同时包含临时会话. |
* | 任意群的任意临时会话 | t* | 必须通过临时会话 |
* | 精确群的任意临时会话 | t123456.* | 123456 内的任意成员. 必须通过临时会话 |
* | 任意好友 | f* | |
* | 任意用户 | u* | 任何人在任何环境 |
* | 任意对象 | * | 即任何人, 任何群, 控制台 |
*
*
* #### 关系图
*
* ``` * ```
* Console AnyContact * Console AnyContact
* *
@ -141,7 +169,7 @@ public interface PermitteeId {
* ExactTemp * ExactTemp
* ``` * ```
*/ */
@Serializable(with = AbstractPermitteeId.AsStringSerializer::class) @Serializable(with = AsStringSerializer::class)
public sealed class AbstractPermitteeId( public sealed class AbstractPermitteeId(
public final override vararg val directParents: PermitteeId, public final override vararg val directParents: PermitteeId,
) : PermitteeId { ) : PermitteeId {

View File

@ -11,7 +11,6 @@
package net.mamoe.mirai.console.plugin package net.mamoe.mirai.console.plugin
import com.vdurmont.semver4j.Semver
import net.mamoe.mirai.console.command.CommandOwner import net.mamoe.mirai.console.command.CommandOwner
import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.disable import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.disable
import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.enable import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.enable
@ -20,6 +19,7 @@ import net.mamoe.mirai.console.plugin.description.PluginDependency
import net.mamoe.mirai.console.plugin.description.PluginDescription import net.mamoe.mirai.console.plugin.description.PluginDescription
import net.mamoe.mirai.console.plugin.jvm.JvmPlugin import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
import net.mamoe.mirai.console.plugin.loader.PluginLoader import net.mamoe.mirai.console.plugin.loader.PluginLoader
import net.mamoe.mirai.console.util.SemVersion
/** /**
* 表示一个 mirai-console 插件. * 表示一个 mirai-console 插件.
@ -62,7 +62,7 @@ public inline val Plugin.name: String get() = this.description.name
/** /**
* 获取 [PluginDescription.version] * 获取 [PluginDescription.version]
*/ */
public inline val Plugin.version: Semver get() = this.description.version public inline val Plugin.version: SemVersion get() = this.description.version
/** /**
* 获取 [PluginDescription.info] * 获取 [PluginDescription.info]

View File

@ -41,28 +41,24 @@ public interface PluginFileExtensions {
* 从数据目录获取一个文件. * 从数据目录获取一个文件.
* @see dataFolderPath * @see dataFolderPath
*/ */
@JvmDefault
public fun resolveDataFile(relativePath: String): File = dataFolderPath.resolve(relativePath).toFile() public fun resolveDataFile(relativePath: String): File = dataFolderPath.resolve(relativePath).toFile()
/** /**
* 从数据目录获取一个文件. * 从数据目录获取一个文件.
* @see dataFolderPath * @see dataFolderPath
*/ */
@JvmDefault
public fun resolveDataPath(relativePath: String): Path = dataFolderPath.resolve(relativePath) public fun resolveDataPath(relativePath: String): Path = dataFolderPath.resolve(relativePath)
/** /**
* 从数据目录获取一个文件. * 从数据目录获取一个文件.
* @see dataFolderPath * @see dataFolderPath
*/ */
@JvmDefault
public fun resolveDataFile(relativePath: Path): File = dataFolderPath.resolve(relativePath).toFile() public fun resolveDataFile(relativePath: Path): File = dataFolderPath.resolve(relativePath).toFile()
/** /**
* 从数据目录获取一个文件路径. * 从数据目录获取一个文件路径.
* @see dataFolderPath * @see dataFolderPath
*/ */
@JvmDefault
public fun resolveDataPath(relativePath: Path): Path = dataFolderPath.resolve(relativePath) public fun resolveDataPath(relativePath: Path): Path = dataFolderPath.resolve(relativePath)
@ -83,27 +79,23 @@ public interface PluginFileExtensions {
* 从配置目录获取一个文件. * 从配置目录获取一个文件.
* @see configFolderPath * @see configFolderPath
*/ */
@JvmDefault
public fun resolveConfigFile(relativePath: String): File = configFolderPath.resolve(relativePath).toFile() public fun resolveConfigFile(relativePath: String): File = configFolderPath.resolve(relativePath).toFile()
/** /**
* 从配置目录获取一个文件. * 从配置目录获取一个文件.
* @see configFolderPath * @see configFolderPath
*/ */
@JvmDefault
public fun resolveConfigPath(relativePath: String): Path = configFolderPath.resolve(relativePath) public fun resolveConfigPath(relativePath: String): Path = configFolderPath.resolve(relativePath)
/** /**
* 从配置目录获取一个文件. * 从配置目录获取一个文件.
* @see configFolderPath * @see configFolderPath
*/ */
@JvmDefault
public fun resolveConfigFile(relativePath: Path): File = configFolderPath.resolve(relativePath).toFile() public fun resolveConfigFile(relativePath: Path): File = configFolderPath.resolve(relativePath).toFile()
/** /**
* 从配置目录获取一个文件路径. * 从配置目录获取一个文件路径.
* @see configFolderPath * @see configFolderPath
*/ */
@JvmDefault
public fun resolveConfigPath(relativePath: Path): Path = configFolderPath.resolve(relativePath) public fun resolveConfigPath(relativePath: Path): Path = configFolderPath.resolve(relativePath)
} }

View File

@ -37,7 +37,6 @@ public interface ResourceContainer {
* *
* @return 资源文件内容. 在未找到文件时返回 `null`. * @return 资源文件内容. 在未找到文件时返回 `null`.
*/ */
@JvmDefault
public fun getResource(path: String): String? = getResource(path, Charsets.UTF_8) public fun getResource(path: String): String? = getResource(path, Charsets.UTF_8)
/** /**
@ -45,7 +44,6 @@ public interface ResourceContainer {
* *
* @return 资源文件内容. 在未找到文件时返回 `null`. * @return 资源文件内容. 在未找到文件时返回 `null`.
*/ */
@JvmDefault
public fun getResource(path: String, charset: Charset): String? = public fun getResource(path: String, charset: Charset): String? =
this.getResourceAsStream(path)?.use(InputStream::readBytes)?.let(::String) this.getResourceAsStream(path)?.use(InputStream::readBytes)?.let(::String)

View File

@ -11,6 +11,10 @@
package net.mamoe.mirai.console.plugin.description package net.mamoe.mirai.console.plugin.description
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.PLUGIN_ID
import net.mamoe.mirai.console.util.SemVersion
/** /**
* 插件的一个依赖的信息. * 插件的一个依赖的信息.
* *
@ -20,16 +24,15 @@ public data class PluginDependency @JvmOverloads constructor(
/** /**
* 依赖插件 ID, [PluginDescription.id] * 依赖插件 ID, [PluginDescription.id]
*/ */
public val id: String, @ResolveContext(PLUGIN_ID) public val id: String,
/** /**
* 依赖版本号. null 时则为不限制版本. * 依赖版本号. null 时则为不限制版本.
* *
* 版本遵循 [语义化版本 2.0 规范](https://semver.org/lang/zh-CN/), * 版本遵循 [语义化版本 2.0 规范](https://semver.org/lang/zh-CN/),
* *
* ### 示例 * @see SemVersion.Requirement
* `Requirement.buildIvy("[1.0, 2.0)")`
*/ */
public val versionRequirement: VersionRequirement? = null, public val versionRequirement: SemVersion.Requirement? = null,
/** /**
* 若为 `false`, 插件在找不到此依赖时也能正常加载. * 若为 `false`, 插件在找不到此依赖时也能正常加载.
*/ */
@ -46,7 +49,10 @@ public data class PluginDependency @JvmOverloads constructor(
/** /**
* @see PluginDependency * @see PluginDependency
*/ */
public constructor(name: String, isOptional: Boolean = false) : this( public constructor(
name, null, isOptional @ResolveContext(PLUGIN_ID) id: String,
isOptional: Boolean = false,
) : this(
id, null, isOptional
) )
} }

View File

@ -9,8 +9,10 @@
package net.mamoe.mirai.console.plugin.description package net.mamoe.mirai.console.plugin.description
import com.vdurmont.semver4j.Semver import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.*
import net.mamoe.mirai.console.plugin.Plugin import net.mamoe.mirai.console.plugin.Plugin
import net.mamoe.mirai.console.util.SemVersion
/** /**
@ -46,6 +48,7 @@ public interface PluginDescription {
* @see ID_REGEX * @see ID_REGEX
* @see FORBIDDEN_ID_NAMES * @see FORBIDDEN_ID_NAMES
*/ */
@ResolveContext(PLUGIN_ID)
public val id: String public val id: String
/** /**
@ -60,6 +63,7 @@ public interface PluginDescription {
* *
* @see FORBIDDEN_ID_NAMES * @see FORBIDDEN_ID_NAMES
*/ */
@ResolveContext(PLUGIN_NAME)
public val name: String public val name: String
/** /**
@ -88,7 +92,8 @@ public interface PluginDescription {
* *
* @see Semver 语义化版本. 允许 [宽松][Semver.SemverType.LOOSE] 类型版本. * @see Semver 语义化版本. 允许 [宽松][Semver.SemverType.LOOSE] 类型版本.
*/ */
public val version: Semver @ResolveContext(PLUGIN_VERSION)
public val version: SemVersion
/** /**
* 插件信息, 允许为空 * 插件信息, 允许为空

View File

@ -1,242 +0,0 @@
package net.mamoe.mirai.console.plugin.description
import com.vdurmont.semver4j.Requirement
import com.vdurmont.semver4j.Semver
public sealed class VersionRequirement {
public abstract operator fun contains(version: Semver): Boolean
public fun contains(version: String): Boolean = contains(Semver(version, Semver.SemverType.LOOSE))
public class Exact
@Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR)
constructor(
version: Semver,
) : VersionRequirement() {
@Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR)
public val version: Semver = version.toStrict()
@Suppress("DEPRECATION_ERROR")
public constructor(version: String) : this(Semver(version, Semver.SemverType.LOOSE))
@Suppress("DEPRECATION_ERROR")
override fun contains(version: Semver): Boolean = this.version.isEquivalentTo(version.toStrict())
}
public data class MatchesNpmPattern(
val pattern: String,
) : VersionRequirement() {
private val requirement = Requirement.buildNPM(pattern)
override fun contains(version: Semver): Boolean = requirement.isSatisfiedBy(version.toStrict())
}
public data class MatchesIvyPattern(
val pattern: String,
) : VersionRequirement() {
private val requirement = Requirement.buildIvy(pattern)
override fun contains(version: Semver): Boolean = requirement.isSatisfiedBy(version.toStrict())
}
public data class MatchesCocoapodsPattern(
val pattern: String,
) : VersionRequirement() {
private val requirement = Requirement.buildCocoapods(pattern)
override fun contains(version: Semver): Boolean = requirement.isSatisfiedBy(version.toStrict())
}
public abstract class Custom : VersionRequirement()
@Suppress("MemberVisibilityCanBePrivate")
public class InRange(
begin: Semver,
public val beginInclusive: Boolean,
end: Semver,
public val endInclusive: Boolean,
) : VersionRequirement() {
public val end: Semver = end.toStrict()
public val begin: Semver = begin.toStrict()
public constructor(
begin: String,
beginInclusive: Boolean,
end: Semver,
endInclusive: Boolean,
) : this(Semver(begin, Semver.SemverType.LOOSE), beginInclusive, end, endInclusive)
public constructor(
begin: String,
beginInclusive: Boolean,
end: String,
endInclusive: Boolean,
) : this(Semver(begin, Semver.SemverType.LOOSE),
beginInclusive,
Semver(end, Semver.SemverType.LOOSE),
endInclusive)
public constructor(
begin: Semver,
beginInclusive: Boolean,
end: String,
endInclusive: Boolean,
) : this(begin, beginInclusive, Semver(end, Semver.SemverType.LOOSE), endInclusive)
override fun contains(version: Semver): Boolean {
val strict = version.toStrict()
return if (beginInclusive) {
strict.isGreaterThanOrEqualTo(begin)
} else {
strict.isGreaterThan(begin)
} && if (endInclusive) {
strict.isLowerThanOrEqualTo(end)
} else {
strict.isLowerThan(end)
}
}
override fun toString(): String {
return buildString {
append(if (beginInclusive) "[" else "(")
append(begin)
append(",")
append(end)
append(if (endInclusive) "]" else ")")
}
}
}
@Suppress("unused", "DeprecatedCallableAddReplaceWith")
public class Builder {
@Suppress("DEPRECATION_ERROR")
@Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR)
@ILoveKafuuChinoForever
public fun exact(version: Semver): VersionRequirement = Exact(version)
@ILoveKafuuChinoForever
public fun exact(version: String): VersionRequirement = Exact(version)
@Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR)
@ILoveKafuuChinoForever
public fun custom(checker: (version: Semver) -> Boolean): VersionRequirement {
return object : Custom() {
override fun contains(version: Semver): Boolean = checker(version)
}
}
/**
* @see Semver.SemverType.NPM
*/
@ILoveKafuuChinoForever
public fun npmPattern(versionPattern: String): VersionRequirement {
return MatchesNpmPattern(versionPattern)
}
/**
* @see Semver.SemverType.IVY
*/
@ILoveKafuuChinoForever
public fun ivyPattern(versionPattern: String): VersionRequirement {
return MatchesIvyPattern(versionPattern)
}
/**
* @see Semver.SemverType.COCOAPODS
*/
@ILoveKafuuChinoForever
public fun cocoapodsPattern(versionPattern: String): VersionRequirement {
return MatchesCocoapodsPattern(versionPattern)
}
@Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR)
@ILoveKafuuChinoForever
public fun range(
begin: Semver,
beginInclusive: Boolean,
end: Semver,
endInclusive: Boolean,
): VersionRequirement = InRange(begin, beginInclusive, end, endInclusive)
@Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR)
@ILoveKafuuChinoForever
public fun range(
begin: String,
beginInclusive: Boolean,
end: Semver,
endInclusive: Boolean,
): VersionRequirement = InRange(begin, beginInclusive, end, endInclusive)
@Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR)
@ILoveKafuuChinoForever
public fun range(
begin: Semver,
beginInclusive: Boolean,
end: String,
endInclusive: Boolean,
): VersionRequirement = InRange(begin, beginInclusive, end, endInclusive)
@ILoveKafuuChinoForever
public fun range(
begin: String,
beginInclusive: Boolean,
end: String,
endInclusive: Boolean,
): VersionRequirement = InRange(begin, beginInclusive, end, endInclusive)
@Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR)
@ILoveKafuuChinoForever
public operator fun Semver.rangeTo(endInclusive: Semver): VersionRequirement {
return InRange(this, true, endInclusive, true)
}
@ILoveKafuuChinoForever
public operator fun Semver.rangeTo(endInclusive: String): VersionRequirement {
return InRange(this, true, Semver(endInclusive, Semver.SemverType.LOOSE), true)
}
@ILoveKafuuChinoForever
public operator fun String.rangeTo(endInclusive: String): VersionRequirement {
return InRange(Semver(this, Semver.SemverType.LOOSE),
true,
Semver(endInclusive, Semver.SemverType.LOOSE),
true)
}
@Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR)
@ILoveKafuuChinoForever
public operator fun String.rangeTo(endInclusive: Semver): VersionRequirement {
return InRange(Semver(this, Semver.SemverType.LOOSE), true, endInclusive, true)
}
@Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR)
@ILoveKafuuChinoForever
public infix fun Semver.until(endExclusive: Semver): VersionRequirement {
return InRange(this, true, endExclusive, false)
}
@ILoveKafuuChinoForever
public infix fun Semver.until(endExclusive: String): VersionRequirement {
return InRange(this, true, Semver(endExclusive, Semver.SemverType.LOOSE), false)
}
@ILoveKafuuChinoForever
public infix fun String.until(endExclusive: String): VersionRequirement {
return InRange(Semver(this, Semver.SemverType.LOOSE),
true,
Semver(endExclusive, Semver.SemverType.LOOSE),
false)
}
@Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR)
@ILoveKafuuChinoForever
public infix fun String.until(endExclusive: Semver): VersionRequirement {
return InRange(Semver(this, Semver.SemverType.LOOSE), true, endExclusive, false)
}
@Suppress("SpellCheckingInspection")
@Retention(AnnotationRetention.SOURCE)
@DslMarker
private annotation class ILoveKafuuChinoForever
}
}

View File

@ -16,6 +16,7 @@ import net.mamoe.mirai.console.data.PluginConfig
import net.mamoe.mirai.console.data.PluginData import net.mamoe.mirai.console.data.PluginData
import net.mamoe.mirai.console.internal.plugin.JvmPluginInternal import net.mamoe.mirai.console.internal.plugin.JvmPluginInternal
import net.mamoe.mirai.console.permission.PermissionId import net.mamoe.mirai.console.permission.PermissionId
import net.mamoe.mirai.console.permission.PermissionService
import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.utils.minutesToMillis import net.mamoe.mirai.utils.minutesToMillis
import net.mamoe.mirai.utils.secondsToMillis import net.mamoe.mirai.utils.secondsToMillis
@ -37,7 +38,8 @@ public abstract class AbstractJvmPlugin @JvmOverloads constructor(
public final override val loader: JvmPluginLoader get() = super<JvmPluginInternal>.loader public final override val loader: JvmPluginLoader get() = super<JvmPluginInternal>.loader
public final override fun permissionId(name: String): PermissionId = PermissionId(description.id, "command.$name") public final override fun permissionId(name: String): PermissionId =
PermissionService.INSTANCE.allocatePermissionIdForPlugin(this, "*", PermissionService.PluginPermissionIdRequestType.PERMISSION_ID)
/** /**
* 重载 [PluginData] * 重载 [PluginData]

View File

@ -18,6 +18,9 @@
package net.mamoe.mirai.console.plugin.jvm package net.mamoe.mirai.console.plugin.jvm
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import net.mamoe.mirai.console.compiler.common.RestrictedScope
import net.mamoe.mirai.console.compiler.common.RestrictedScope.Kind.COMMAND_REGISTER
import net.mamoe.mirai.console.compiler.common.RestrictedScope.Kind.PERMISSION_REGISTER
import net.mamoe.mirai.console.extension.PluginComponentStorage import net.mamoe.mirai.console.extension.PluginComponentStorage
import net.mamoe.mirai.console.permission.PermissionIdNamespace import net.mamoe.mirai.console.permission.PermissionIdNamespace
import net.mamoe.mirai.console.plugin.Plugin import net.mamoe.mirai.console.plugin.Plugin
@ -59,6 +62,7 @@ public interface JvmPlugin : Plugin, CoroutineScope,
* *
* @receiver 组件容器 * @receiver 组件容器
*/ */
@RestrictedScope(COMMAND_REGISTER, PERMISSION_REGISTER)
public fun PluginComponentStorage.onLoad() {} public fun PluginComponentStorage.onLoad() {}
/** /**

View File

@ -11,10 +11,11 @@
package net.mamoe.mirai.console.plugin.jvm package net.mamoe.mirai.console.plugin.jvm
import com.vdurmont.semver4j.Semver import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.*
import net.mamoe.mirai.console.plugin.description.PluginDependency import net.mamoe.mirai.console.plugin.description.PluginDependency
import net.mamoe.mirai.console.plugin.description.PluginDescription import net.mamoe.mirai.console.plugin.description.PluginDescription
import net.mamoe.mirai.console.plugin.description.VersionRequirement import net.mamoe.mirai.console.util.SemVersion
/** /**
* JVM 插件的描述. 通常作为 `plugin.yml` * JVM 插件的描述. 通常作为 `plugin.yml`
@ -32,20 +33,21 @@ public interface JvmPluginDescription : PluginDescription {
* 构建 [JvmPluginDescription] * 构建 [JvmPluginDescription]
* @see JvmPluginDescriptionBuilder * @see JvmPluginDescriptionBuilder
*/ */
@JvmName("create")
@JvmSynthetic @JvmSynthetic
public operator fun invoke( public operator fun invoke(
/** /**
* @see [PluginDescription.id] * @see [PluginDescription.id]
*/ */
id: String, @ResolveContext(PLUGIN_ID) id: String,
/** /**
* @see [PluginDescription.version] * @see [PluginDescription.version]
*/ */
version: String, @ResolveContext(PLUGIN_VERSION) version: String,
/** /**
* @see [PluginDescription.name] * @see [PluginDescription.name]
*/ */
name: String = id, @ResolveContext(PLUGIN_NAME) name: String = id,
block: JvmPluginDescriptionBuilder.() -> Unit = {}, block: JvmPluginDescriptionBuilder.() -> Unit = {},
): JvmPluginDescription = JvmPluginDescriptionBuilder(id, version).apply { name(name) }.apply(block).build() ): JvmPluginDescription = JvmPluginDescriptionBuilder(id, version).apply { name(name) }.apply(block).build()
@ -53,22 +55,21 @@ public interface JvmPluginDescription : PluginDescription {
* 构建 [JvmPluginDescription] * 构建 [JvmPluginDescription]
* @see JvmPluginDescriptionBuilder * @see JvmPluginDescriptionBuilder
*/ */
@Suppress("DEPRECATION_ERROR") @JvmName("create")
@Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR)
@JvmSynthetic @JvmSynthetic
public operator fun invoke( public operator fun invoke(
/** /**
* @see [PluginDescription.id] * @see [PluginDescription.id]
*/ */
id: String, @ResolveContext(PLUGIN_ID) id: String,
/** /**
* @see [PluginDescription.version] * @see [PluginDescription.version]
*/ */
version: Semver, @ResolveContext(PLUGIN_VERSION) version: SemVersion,
/** /**
* @see [PluginDescription.name] * @see [PluginDescription.name]
*/ */
name: String = id, @ResolveContext(PLUGIN_NAME) name: String = id,
block: JvmPluginDescriptionBuilder.() -> Unit = {}, block: JvmPluginDescriptionBuilder.() -> Unit = {},
): JvmPluginDescription = JvmPluginDescriptionBuilder(id, version).apply { name(name) }.apply(block).build() ): JvmPluginDescription = JvmPluginDescriptionBuilder(id, version).apply { name(name) }.apply(block).build()
} }
@ -95,14 +96,14 @@ public interface JvmPluginDescription : PluginDescription {
* *
* @see [JvmPluginDescription.invoke] * @see [JvmPluginDescription.invoke]
*/ */
public class JvmPluginDescriptionBuilder public class JvmPluginDescriptionBuilder(
@Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR)
constructor(
private var id: String, private var id: String,
private var version: Semver, private var version: SemVersion,
) { ) {
@Suppress("DEPRECATION_ERROR") public constructor(
public constructor(name: String, version: String) : this(name, Semver(version, Semver.SemverType.LOOSE)) @ResolveContext(PLUGIN_ID) id: String,
@ResolveContext(PLUGIN_VERSION) version: String,
) : this(id, SemVersion(version))
private var name: String = id private var name: String = id
private var author: String = "" private var author: String = ""
@ -110,19 +111,20 @@ constructor(
private var dependencies: MutableSet<PluginDependency> = mutableSetOf() private var dependencies: MutableSet<PluginDependency> = mutableSetOf()
@ILoveKuriyamaMiraiForever @ILoveKuriyamaMiraiForever
public fun name(value: String): JvmPluginDescriptionBuilder = apply { this.name = value.trim() } public fun name(@ResolveContext(PLUGIN_NAME) value: String): JvmPluginDescriptionBuilder =
apply { this.name = value.trim() }
@Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR)
@ILoveKuriyamaMiraiForever
public fun version(value: String): JvmPluginDescriptionBuilder =
apply { this.version = Semver(value, Semver.SemverType.LOOSE) }
@Deprecated("Semver 将会在 1.0-RC 被替换为 Console 自己实现的版本。请临时使用 String。", level = DeprecationLevel.ERROR)
@ILoveKuriyamaMiraiForever
public fun version(value: Semver): JvmPluginDescriptionBuilder = apply { this.version = value }
@ILoveKuriyamaMiraiForever @ILoveKuriyamaMiraiForever
public fun id(value: String): JvmPluginDescriptionBuilder = apply { this.id = value.trim() } public fun version(@ResolveContext(PLUGIN_VERSION) value: String): JvmPluginDescriptionBuilder =
apply { this.version = SemVersion(value) }
@ILoveKuriyamaMiraiForever
public fun version(@ResolveContext(PLUGIN_VERSION) value: SemVersion): JvmPluginDescriptionBuilder =
apply { this.version = value }
@ILoveKuriyamaMiraiForever
public fun id(@ResolveContext(PLUGIN_ID) value: String): JvmPluginDescriptionBuilder =
apply { this.id = value.trim() }
@ILoveKuriyamaMiraiForever @ILoveKuriyamaMiraiForever
public fun author(value: String): JvmPluginDescriptionBuilder = apply { this.author = value.trim() } public fun author(value: String): JvmPluginDescriptionBuilder = apply { this.author = value.trim() }
@ -151,9 +153,9 @@ constructor(
*/ */
@ILoveKuriyamaMiraiForever @ILoveKuriyamaMiraiForever
public fun dependsOn( public fun dependsOn(
pluginId: String, @ResolveContext(PLUGIN_ID) pluginId: String,
isOptional: Boolean = false, isOptional: Boolean = false,
versionRequirement: VersionRequirement, versionRequirement: SemVersion.Requirement,
): JvmPluginDescriptionBuilder = apply { ): JvmPluginDescriptionBuilder = apply {
this.dependencies.add(PluginDependency(pluginId, versionRequirement, isOptional)) this.dependencies.add(PluginDependency(pluginId, versionRequirement, isOptional))
} }
@ -165,8 +167,8 @@ constructor(
*/ */
@ILoveKuriyamaMiraiForever @ILoveKuriyamaMiraiForever
public fun dependsOn( public fun dependsOn(
pluginId: String, @ResolveContext(PLUGIN_ID) pluginId: String,
versionRequirement: VersionRequirement, versionRequirement: SemVersion.Requirement,
): JvmPluginDescriptionBuilder = apply { ): JvmPluginDescriptionBuilder = apply {
this.dependencies.add(PluginDependency(pluginId, versionRequirement, false)) this.dependencies.add(PluginDependency(pluginId, versionRequirement, false))
} }
@ -178,46 +180,24 @@ constructor(
*/ */
@ILoveKuriyamaMiraiForever @ILoveKuriyamaMiraiForever
public fun dependsOn( public fun dependsOn(
pluginId: String, @ResolveContext(PLUGIN_ID) pluginId: String,
isOptional: Boolean = false, isOptional: Boolean = false,
): JvmPluginDescriptionBuilder = apply { ): JvmPluginDescriptionBuilder = apply {
this.dependencies.add(PluginDependency(pluginId, null, isOptional)) this.dependencies.add(PluginDependency(pluginId, null, isOptional))
} }
/**
* 示例:
*
* ```
* dependsOn("org.example.test-plugin") { "1.0.0".."1.2.0" }
* dependsOn("org.example.test-plugin") { npmPattern("1.x || >=2.5.0 || 5.0.0 - 7.2.3") }
* dependsOn("org.example.test-plugin") { ivyPattern("[1.0,2.0[") }
* dependsOn("org.example.test-plugin") { custom { it.toString() == "1.0.0" } }
* ```
*
* @see PluginDependency
* @see VersionRequirement.Builder
*/
@ILoveKuriyamaMiraiForever
public fun dependsOn(
pluginId: String,
isOptional: Boolean = false,
versionRequirement: VersionRequirement.Builder.() -> VersionRequirement,
): JvmPluginDescriptionBuilder =
apply {
this.dependencies.add(PluginDependency(pluginId,
VersionRequirement.Builder().run(versionRequirement),
isOptional))
}
@Suppress("DEPRECATION_ERROR") @Suppress("DEPRECATION_ERROR")
public fun build(): JvmPluginDescription = public fun build(): JvmPluginDescription =
SimpleJvmPluginDescription(name, version, id, author, info, dependencies) SimpleJvmPluginDescription(name, version, id, author, info, dependencies)
/**
* 标注一个 [JvmPluginDescription] DSL
*/
@Suppress("SpellCheckingInspection") @Suppress("SpellCheckingInspection")
@Retention(AnnotationRetention.SOURCE) @Retention(AnnotationRetention.BINARY)
@DslMarker @DslMarker
private annotation class ILoveKuriyamaMiraiForever // https://zh.moegirl.org.cn/zh-cn/%E6%A0%97%E5%B1%B1%E6%9C%AA%E6%9D%A5 internal annotation class ILoveKuriyamaMiraiForever // https://zh.moegirl.org.cn/zh-cn/%E6%A0%97%E5%B1%B1%E6%9C%AA%E6%9D%A5
} }
/** /**
@ -225,56 +205,26 @@ constructor(
* *
* @see JvmPluginDescription * @see JvmPluginDescription
*/ */
@Deprecated( internal data class SimpleJvmPluginDescription
""" @JvmOverloads constructor(
将在 1.0-RC 删除. 请使用 JvmPluginDescription. override val name: String,
""", override val version: SemVersion,
replaceWith = ReplaceWith( override val id: String = name,
"JvmPluginDescription", override val author: String = "",
"net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription" override val info: String = "",
), override val dependencies: Set<PluginDependency> = setOf(),
level = DeprecationLevel.ERROR
)
public data class SimpleJvmPluginDescription
@Deprecated(
"""
构造器不稳定, 将在 1.0-RC 删除. 请使用 JvmPluginDescriptionBuilder.
""",
replaceWith = ReplaceWith(
"JvmPluginDescription(name, version) {}",
"net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription.Companion.invoke"
),
level = DeprecationLevel.ERROR
)
@JvmOverloads public constructor(
public override val name: String,
public override val version: Semver,
public override val id: String = name,
public override val author: String = "",
public override val info: String = "",
public override val dependencies: Set<PluginDependency> = setOf(),
) : JvmPluginDescription { ) : JvmPluginDescription {
@Deprecated(
"""
构造器不稳定, 将在 1.0-RC 删除. 请使用 JvmPluginDescriptionBuilder.
""",
replaceWith = ReplaceWith(
"JvmPluginDescription.invoke(name, version) {}",
"net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription.Companion.invoke"
),
level = DeprecationLevel.ERROR
)
@Suppress("DEPRECATION_ERROR") @Suppress("DEPRECATION_ERROR")
@JvmOverloads @JvmOverloads
public constructor( constructor(
name: String, name: String,
version: String, version: String,
id: String = name, id: String = name,
author: String = "", author: String = "",
info: String = "", info: String = "",
dependencies: Set<PluginDependency> = setOf(), dependencies: Set<PluginDependency> = setOf(),
) : this(name, Semver(version, Semver.SemverType.LOOSE), id, author, info, dependencies) ) : this(name, SemVersion(version), id, author, info, dependencies)
init { init {
require(!name.contains(':')) { "':' is forbidden in plugin name" } require(!name.contains(':')) { "':' is forbidden in plugin name" }

View File

@ -15,7 +15,7 @@ import net.mamoe.mirai.console.internal.plugin.BuiltInJvmPluginLoaderImpl
import net.mamoe.mirai.console.plugin.loader.FilePluginLoader import net.mamoe.mirai.console.plugin.loader.FilePluginLoader
/** /**
* 内建的 Jar (JVM) 插件加载器 * JVM 插件加载器
*/ */
public interface JvmPluginLoader : CoroutineScope, FilePluginLoader<JvmPlugin, JvmPluginDescription> { public interface JvmPluginLoader : CoroutineScope, FilePluginLoader<JvmPlugin, JvmPluginDescription> {
/** /**

View File

@ -61,10 +61,9 @@ public interface PluginLoader<P : Plugin, D : PluginDescription> {
* @throws PluginLoadException 在加载插件遇到意料之中的错误时抛出 (如无法读取插件信息等). * @throws PluginLoadException 在加载插件遇到意料之中的错误时抛出 (如无法读取插件信息等).
* *
* @see PluginDescription 插件描述 * @see PluginDescription 插件描述
* @see getPluginDescription receiver, 接受参数的版本.
*/ */
@Throws(PluginLoadException::class) @Throws(PluginLoadException::class)
public fun getPluginDescription(plugin: P): D // Java signature: `public D getDescription(P)` public fun getPluginDescription(plugin: P): D
/** /**
* 主动加载一个插件 (实例), 但不 [启用][enable] . 返回加载成功的主类实例 * 主动加载一个插件 (实例), 但不 [启用][enable] . 返回加载成功的主类实例

View File

@ -96,7 +96,7 @@ import kotlin.internal.LowPriorityInOverloadResolution
*/ */
public interface MessageScope { public interface MessageScope {
/** /**
* 如果此 [MessageScope], 仅包含一个消息对象, [realTarget] 指向这个对象. * 如果此 [MessageScope] 仅包含一个消息对象, [realTarget] 指向这个对象. 否则 [realTarget] `null`.
* *
* 对于 [CommandSender] 作为 [MessageScope], [realTarget] 总是指令执行者 [User], [CommandSender.user] * 对于 [CommandSender] 作为 [MessageScope], [realTarget] 总是指令执行者 [User], [CommandSender.user]
* *
@ -116,7 +116,6 @@ public interface MessageScope {
/** /**
* 立刻以此发送消息给所有在此 [MessageScope] 下的消息对象 * 立刻以此发送消息给所有在此 [MessageScope] 下的消息对象
*/ */
@JvmDefault
@JvmBlockingBridge @JvmBlockingBridge
public suspend fun sendMessage(message: String) public suspend fun sendMessage(message: String)
} }

View File

@ -0,0 +1,214 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*
*/
/*
* @author Karlatemp <karlatemp@vip.qq.com> <https://github.com/Karlatemp>
*/
@file:Suppress("unused")
package net.mamoe.mirai.console.util
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.builtins.serializer
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.PLUGIN_VERSION
import net.mamoe.mirai.console.internal.data.map
import net.mamoe.mirai.console.internal.util.semver.SemVersionInternal
import net.mamoe.mirai.console.util.SemVersion.Companion.equals
import net.mamoe.mirai.console.util.SemVersion.Requirement
/**
* [语义化版本](https://semver.org/lang/zh-CN/) 支持
*
* 解析示例:
*
* `1.0.0-M4+c25733b8` 将会解析出下面的内容,
* [major] (主本号), [minor] (次版本号), [patch] (修订号), [identifier] (先行版本号) [metadata] (元数据).
* ```
* SemVersion(
* major = 1,
* minor = 0,
* patch = 0,
* identifier = "M4"
* metadata = "c25733b8"
* )
* ```
* 其中 identifier metadata 都是可选的.
*
* 对于核心版本号, 此实现稍微比 semver 宽松一些, 允许 x.y 的存在.
*
* @see Requirement
* @see SemVersion.invoke
*/
@Serializable(with = SemVersion.SemVersionAsStringSerializer::class)
public data class SemVersion
/**
* @see SemVersion.invoke 字符串解析
*/
internal constructor(
/** 主版本号 */
public val major: Int,
/** 次版本号 */
public val minor: Int,
/** 修订号 */
public val patch: Int?,
/** 先行版本号识别符 */
public val identifier: String? = null,
/** 版本号元数据, 不参与版本号对比([compareTo]), 但是参与版本号严格对比([equals]) */
public val metadata: String? = null,
) : Comparable<SemVersion> {
/**
* 一条依赖规则
* @see [parseRangeRequirement]
*/
public interface Requirement {
/** 在 [version] 满足此要求时返回 true */
public fun test(version: SemVersion): Boolean
}
public object SemVersionAsStringSerializer : KSerializer<SemVersion> by String.serializer().map(
serializer = { it.toString() },
deserializer = { SemVersion(it) }
)
public companion object {
/**
* 解析一个版本号, 将会返回一个 [SemVersion],
* 如果发生解析错误将会抛出一个 [IllegalArgumentException] 或者 [NumberFormatException]
*
* 对于版本号的组成, 有以下规定:
* - 必须包含主版本号和次版本号
* - 存在 先行版本号 的时候 先行版本号 不能为空
* - 存在 元数据 的时候 元数据 不能为空
* - 核心版本号只允许 `x.y` `x.y.z` 的存在
* - `1.0-RC` 是合法的
* - `1.0.0-RC` 也是合法的, `1.0-RC` 一样
* - `1.0.0.0-RC` 是不合法的, 将会抛出一个 [IllegalArgumentException]
*
* 注意情况:
* - 第一个 `+` 之后的所有内容全部识别为元数据
* - `1.0+METADATA-M4`, metadata="METADATA-M4"
* - 如果不确定版本号是否合法, 可以使用 [regex101.com](https://regex101.com/r/vkijKf/1/) 进行检查
* - 此实现使用的正则表达式为 `^(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`
*/
@Throws(IllegalArgumentException::class, NumberFormatException::class)
@JvmStatic
@JvmName("parse")
public operator fun invoke(@ResolveContext(PLUGIN_VERSION) version: String): SemVersion = SemVersionInternal.parse(version)
/**
* 解析一条依赖需求描述, 在无法解析的时候抛出 [IllegalArgumentException]
*
* 对于一条规则, 有以下方式可选
*
* - `1.0.0-M4` 要求 1.0.0-M4 版本, 且只能是 1.0.0-M4 版本
* - `1.x` 要求 1.x 版本
* - `> 1.0.0-RC` 要求 1.0.0-RC 之后的版本, 不能是 1.0.0-RC
* - `>= 1.0.0-RC` 要求 1.0.0-RC 或之后的版本, 可以是 1.0.0-RC
* - `< 1.0.0-RC` 要求 1.0.0-RC 之前的版本, 不能是 1.0.0-RC
* - `<= 1.0.0-RC` 要求 1.0.0-RC 或之前的版本, 可以是 1.0.0-RC
* - `!= 1.0.0-RC` 要求 除了1.0.0-RC 的任何版本
* - `[1.0.0, 1.2.0]`
* - `(1.0.0, 1.2.0]`
* - `[1.0.0, 1.2.0)`
* - `(1.0.0, 1.2.0)` [数学区间](https://baike.baidu.com/item/%E5%8C%BA%E9%97%B4/1273117)
*
* 对于多个规则, 允许使用逻辑符号 `{}`, `||`, `&&`
* 例如:
* - `1.x || 2.x || 3.0.0`
* - `<= 0.5.3 || >= 1.0.0`
* - `{> 1.0 && < 1.5} || {> 1.8}`
* - `{> 1.0 && < 1.5} || {> 1.8}`
* - `> 1.0.0 && != 1.2.0`
*
* 特别注意:
* - 依赖规则版本号不需要携带版本号元数据, 元数据不参与依赖需求的检查
* - 如果目标版本号携带有先行版本号, 请不要忘记先行版本号
* - 因为 `()` 已经用于数学区间, 使用 `{}` 替代 `()`
*/
@Throws(IllegalArgumentException::class)
@JvmStatic
public fun parseRangeRequirement(requirement: String): Requirement =
SemVersionInternal.parseRangeRequirement(requirement)
/** @see [Requirement.test] */
@JvmStatic
public fun Requirement.test(@ResolveContext(PLUGIN_VERSION) version: String): Boolean = test(invoke(version))
/**
* 当满足 [requirement] 时返回 true, 否则返回 false
*/
@JvmStatic
public fun SemVersion.satisfies(requirement: Requirement): Boolean = requirement.test(this)
/** for Kotlin only */
@JvmStatic
@JvmSynthetic
public operator fun Requirement.contains(version: SemVersion): Boolean = test(version)
/** for Kotlin only */
@JvmStatic
@JvmSynthetic
public operator fun Requirement.contains(@ResolveContext(PLUGIN_VERSION) version: String): Boolean = test(version)
}
@Transient
private val toString: String by lazy(LazyThreadSafetyMode.NONE) {
buildString {
append(major)
append('.').append(minor)
patch?.let { append('.').append(it) }
identifier?.let { identifier ->
append('-').append(identifier)
}
metadata?.let { metadata ->
append('+').append(metadata)
}
}
}
override fun toString(): String = toString
/**
* [SemVersion] 转为 Kotlin data class 风格的 [String]
*/
public fun toStructuredString(): String {
return "SemVersion(major=$major, minor=$minor, patch=$patch, identifier=$identifier, metadata=$metadata)"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SemVersion
return compareTo(other) == 0 && other.identifier == identifier && other.metadata == metadata
}
override fun hashCode(): Int {
var result = major shl minor
result *= (patch ?: 1)
result = 31 * result + (identifier?.hashCode() ?: 0)
result = 31 * result + (metadata?.hashCode() ?: 0)
return result
}
/**
* Compares this object with the specified object for order. Returns zero if this object is equal
* to the specified [other] object, a negative number if it's less than [other], or a positive number
* if it's greater than [other].
*/
public override operator fun compareTo(other: SemVersion): Int {
return SemVersionInternal.run { compareInternal(this@SemVersion, other) }
}
}

View File

@ -9,7 +9,6 @@
package net.mamoe.mirai.console package net.mamoe.mirai.console
import com.vdurmont.semver4j.Semver
import kotlinx.coroutines.* import kotlinx.coroutines.*
import net.mamoe.mirai.console.MiraiConsoleImplementation.Companion.start import net.mamoe.mirai.console.MiraiConsoleImplementation.Companion.start
import net.mamoe.mirai.console.command.CommandManager import net.mamoe.mirai.console.command.CommandManager
@ -20,6 +19,7 @@ import net.mamoe.mirai.console.plugin.loader.PluginLoader
import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.util.ConsoleExperimentalApi
import net.mamoe.mirai.console.util.ConsoleInput import net.mamoe.mirai.console.util.ConsoleInput
import net.mamoe.mirai.console.util.ConsoleInternalApi import net.mamoe.mirai.console.util.ConsoleInternalApi
import net.mamoe.mirai.console.util.SemVersion
import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.utils.BotConfiguration import net.mamoe.mirai.utils.BotConfiguration
import net.mamoe.mirai.utils.LoginSolver import net.mamoe.mirai.utils.LoginSolver
@ -43,8 +43,8 @@ fun initTestEnvironment() {
get() = "Test" get() = "Test"
override val vendor: String override val vendor: String
get() = "Test" get() = "Test"
override val version: Semver override val version: SemVersion
get() = Semver("1.0.0") get() = SemVersion("1.0.0")
} }
override val builtInPluginLoaders: List<Lazy<PluginLoader<*, *>>> = listOf(lazy { JvmPluginLoader }) override val builtInPluginLoaders: List<Lazy<PluginLoader<*, *>>> = listOf(lazy { JvmPluginLoader })

View File

@ -79,7 +79,7 @@ internal class TestCommand {
assertEquals(1, ConsoleCommandOwner.registeredCommands.size) assertEquals(1, ConsoleCommandOwner.registeredCommands.size)
assertEquals(1, CommandManagerImpl.registeredCommands.size) assertEquals(1, CommandManagerImpl._registeredCommands.size)
assertEquals(2, CommandManagerImpl.requiredPrefixCommandMap.size) assertEquals(2, CommandManagerImpl.requiredPrefixCommandMap.size)
} finally { } finally {
TestCompositeCommand.unregister() TestCompositeCommand.unregister()

View File

@ -17,7 +17,7 @@ import kotlin.test.assertSame
@OptIn(ConsoleInternalApi::class) @OptIn(ConsoleInternalApi::class)
internal class PluginDataTest { internal class PluginDataTest {
class MyPluginData : AutoSavePluginData() { class MyPluginData : AutoSavePluginData("test") {
var int by value(1) var int by value(1)
val map: MutableMap<String, String> by value() val map: MutableMap<String, String> by value()
val map2: MutableMap<String, MutableMap<String, String>> by value() val map2: MutableMap<String, MutableMap<String, String>> by value()

View File

@ -0,0 +1,225 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*
*/
/*
* @author Karlatemp <karlatemp@vip.qq.com> <https://github.com/Karlatemp>
*/
package net.mamoe.mirai.console.util
import net.mamoe.mirai.console.util.SemVersion.Companion.test
import org.junit.jupiter.api.Test
internal class TestSemVersion {
@Test
internal fun testCompare() {
fun String.sem(): SemVersion = SemVersion.invoke(this)
assert("1.0".sem() < "1.0.1".sem())
assert("1.0.0".sem() == "1.0".sem())
assert("1.1".sem() > "1.0.0".sem())
assert("1.0-M4".sem() < "1.0-M5".sem())
assert("1.0-M5-dev-7".sem() < "1.0-M5-dev-15".sem())
assert("1.0-M5-dev-79".sem() < "1.0-M5-dev-7001".sem())
assert("1.0-M6".sem() > "1.0-M5-dev-15".sem())
assert("1.0-RC".sem() > "1.0-M5-dev-15".sem())
assert("1.0-RC2".sem() > "1.0-RC".sem())
// example on semver
// 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0
assert("1.0.0-alpha".sem() < "1.0.0-alpha.1".sem())
assert("1.0.0-alpha.1".sem() < "1.0.0-alpha.beta".sem())
assert("1.0.0-alpha.beta".sem() < "1.0.0-beta".sem())
assert("1.0.0-beta".sem() < "1.0.0-beta.2".sem())
assert("1.0.0-beta.2".sem() < "1.0.0-beta.11".sem())
assert("1.0.0-beta.11".sem() < "1.0.0-rc.1".sem())
assert("1.0.0-rc.1".sem() < "1.0.0".sem())
}
@Test
internal fun testRequirement() {
fun SemVersion.Requirement.assert(version: String): SemVersion.Requirement {
assert(test(version)) { version }
return this
}
fun assertInvalid(requirement: String) {
kotlin.runCatching {
SemVersion.parseRangeRequirement(requirement)
}.onSuccess { assert(false) { requirement } }
}
fun SemVersion.Requirement.assertFalse(version: String): SemVersion.Requirement {
assert(!test(version)) { version }
return this
}
SemVersion.parseRangeRequirement("1.0")
.assert("1.0").assert("1.0.0")
.assertFalse("1.1.0").assertFalse("2.0.0")
SemVersion.parseRangeRequirement("1.x")
.assert("1.0").assert("1.1")
.assert("1.5").assert("1.14514")
.assertFalse("2.33")
SemVersion.parseRangeRequirement("2.0||1.2.x")
SemVersion.parseRangeRequirement("{2.0||1.2.x} && 1.1.0 &&1.2.3")
SemVersion.parseRangeRequirement("2.0 || 1.2.x")
.assert("2.0").assert("2.0.0")
.assertFalse("2.1")
.assert("1.2.5").assert("1.2.0").assertFalse("1.2")
.assertFalse("1.0.0")
SemVersion.parseRangeRequirement("[1.0.0, 19190.0]")
.assert("1.0.0").assertFalse("0.1.0")
.assert("19190.0").assertFalse("19198.10")
SemVersion.parseRangeRequirement("[1.0.0, 2.0.0)")
.assert("1.0.0").assert("1.2.3").assertFalse("2.0.0")
SemVersion.parseRangeRequirement("(2.0.0, 1.0.0]")
.assert("1.0.0").assert("1.2.3").assertFalse("2.0.0")
SemVersion.parseRangeRequirement("(2.0.0, 1.0.0)")
.assertFalse("1.0.0").assert("1.2.3").assertFalse("2.0.0")
SemVersion.parseRangeRequirement("(1.0.0, 2.0.0)")
.assertFalse("1.0.0").assert("1.2.3").assertFalse("2.0.0")
SemVersion.parseRangeRequirement(" >= 1.0.0")
.assert("1.0.0")
.assert("114.514.1919")
.assertFalse("0.0.0")
.assertFalse("0.98774587")
SemVersion.parseRangeRequirement("> 1.0.0")
.assertFalse("1.0.0")
SemVersion.parseRangeRequirement("!= 1.0.0 && != 2.0.0")
.assert("1.2.3").assert("2.1.1")
.assertFalse("1.0").assertFalse("1.0.0")
.assertFalse("2.0").assertFalse("2.0.0")
.assert("2.0.1").assert("1.0.1")
SemVersion.parseRangeRequirement("> 1.0.0 || < 0.9.0")
.assertFalse("1.0.0")
.assert("0.8.0")
.assertFalse("0.9.0")
SemVersion.parseRangeRequirement("{>= 1.0.0 && <= 1.2.3} || {>= 2.0.0 && <= 2.2.3}")
.assertFalse("1.3.0")
.assert("1.0.0").assert("1.2.3")
.assertFalse("0.9.0")
.assert("2.0.0").assert("2.2.3").assertFalse("2.3.4")
assertInvalid("WPOXAXW")
assertInvalid("1.0.0 || 1.0.0 && 1.0.0")
assertInvalid("{")
assertInvalid("}")
assertInvalid("")
assertInvalid("1.2.3 - 3.2.1")
assertInvalid("1.5.78 &&")
assertInvalid("|| 1.0.0")
}
private fun String.check() {
val sem = SemVersion.invoke(this)
assert(this == sem.toString()) { "$this != $sem" }
}
private fun String.checkInvalid() {
kotlin.runCatching { SemVersion.invoke(this) }
.onSuccess { assert(false) { "$this not a invalid sem-version" } }
}
@Test
internal fun testSemVersionParsing() {
"0.0".check()
"1.0.0".check()
"1.2.3.4.5.6.7.8".checkInvalid()
"5555.0-A".check()
"5555.0-A+METADATA".check()
"5555.0+METADATA".check()
"987.0+wwwxx-wk".check()
"NOT.NUMBER".checkInvalid()
"0".checkInvalid()
"".checkInvalid()
"1.".checkInvalid()
"0.1-".checkInvalid()
"1.9+".checkInvalid()
"5.1+68-7".check()
"5.1+68-".check()
}
@Test
internal fun testSemVersionOfficial() {
"""
1.0-RC
0.0.4
1.2.3
10.20.30
1.1.2-prerelease+meta
1.1.2+meta
1.1.2+meta-valid
1.0.0-alpha
1.0.0-beta
1.0.0-alpha.beta
1.0.0-alpha.beta.1
1.0.0-alpha.1
1.0.0-alpha0.valid
1.0.0-alpha.0valid
1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay
1.0.0-rc.1+build.1
2.0.0-rc.1+build.123
1.2.3-beta
10.2.3-DEV-SNAPSHOT
1.2.3-SNAPSHOT-123
1.0.0
2.0.0
1.1.7
2.0.0+build.1848
2.0.1-alpha.1227
1.0.0-alpha+beta
1.2.3----RC-SNAPSHOT.12.9.1--.12+788
1.2.3----R-S.12.9.1--.12+meta
1.2.3----RC-SNAPSHOT.12.9.1--.12
1.0.0+0.build.1-rc.10000aaa-kk-0.1
1.0.0-0A.is.legal
""".trimIndent().split('\n').asSequence()
.filter { it.isNotBlank() }.map { it.trim() }.forEach { it.check() }
"""
1
1.2.3-0123
1.2.3-0123.0123
1.1.2+.123
+invalid
-invalid
-invalid+invalid
-invalid.01
alpha
alpha.beta
alpha.beta.1
alpha.1
alpha+beta
alpha_beta
alpha.
alpha..
beta
1.0.0-alpha_beta
-alpha.
1.0.0-alpha..
1.0.0-alpha..1
1.0.0-alpha...1
1.0.0-alpha....1
1.0.0-alpha.....1
1.0.0-alpha......1
1.0.0-alpha.......1
01.1.1
1.01.1
1.1.01
1.2.3.DEV
1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788
1.2.31.2.3-RC
-1.0.3-gamma+b7718
+justmeta
9.8.7+meta+meta
9.8.7-whatever+meta+meta
99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12
""".trimIndent().split('\n').asSequence()
.filter { it.isNotBlank() }.map { it.trim() }.forEach { it.checkInvalid() }
}
}

View File

@ -1,9 +1,12 @@
@file:Suppress("UnstableApiUsage") @file:Suppress("UnstableApiUsage")
plugins { plugins {
id("com.jfrog.bintray") version Versions.bintray apply false
id("net.mamoe.kotlin-jvm-blocking-bridge") version Versions.blockingBridge apply false
kotlin("jvm") version Versions.kotlinCompiler kotlin("jvm") version Versions.kotlinCompiler
kotlin("plugin.serialization") version Versions.kotlinCompiler kotlin("plugin.serialization") version Versions.kotlinCompiler
id("com.jfrog.bintray") version Versions.bintray apply false
id("net.mamoe.kotlin-jvm-blocking-bridge") version Versions.blockingBridge apply false
id("com.gradle.plugin-publish") version "0.12.0" apply false
//id("com.bmuschko.nexus") version "2.3.1" apply false
//id("io.codearte.nexus-staging") version "0.11.0" apply false
} }
tasks.withType(JavaCompile::class.java) { tasks.withType(JavaCompile::class.java) {

View File

@ -36,8 +36,8 @@ class MiraiConsoleBuildPlugin : Plugin<Project> {
attributes( attributes(
"Manifest-Version" to "1", "Manifest-Version" to "1",
"Implementation-Vendor" to "Mamoe Technologies", "Implementation-Vendor" to "Mamoe Technologies",
"Implementation-Title" to target.name.toString(), "Implementation-Title" to target.name,
"Implementation-Version" to target.version.toString() + "-" + gitVersion "Implementation-Version" to target.version.toString() //+ "+" + gitVersion
) )
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@ -120,15 +120,16 @@ fun Project.findLatestFile(): Pair<String, File> {
} ?: error("cannot find any file to upload")*/ } ?: error("cannot find any file to upload")*/
} }
/*
val gitVersion: String by lazy { val gitVersion: String by lazy {
runCatching { runCatching {
val exec = Runtime.getRuntime().exec("git rev-parse HEAD") val exec = Runtime.getRuntime().exec("git rev-parse HEAD")
exec.waitFor() exec.waitFor()
exec.inputStream.readBytes().toString(Charsets.UTF_8).trim().also { exec.inputStream.readBytes().toString(Charsets.UTF_8).trim().also {
println("Git commit id: $it") println("Git commit id: $it")
} } }.onFailure {
}.onFailure {
it.printStackTrace() it.printStackTrace()
return@lazy "UNKNOWN" return@lazy "UNKNOWN"
}.getOrThrow() }.getOrThrow()
} }
*/

View File

@ -7,6 +7,9 @@ import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.bundling.Jar import org.gradle.api.tasks.bundling.Jar
import org.gradle.kotlin.dsl.* import org.gradle.kotlin.dsl.*
import upload.Bintray import upload.Bintray
import java.io.InputStream
import java.io.OutputStream
import java.security.MessageDigest
import java.util.* import java.util.*
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
@ -51,10 +54,48 @@ internal fun org.gradle.api.Project.`publishing`(configure: org.gradle.api.publi
(this as org.gradle.api.plugins.ExtensionAware).extensions.configure("publishing", configure) (this as org.gradle.api.plugins.ExtensionAware).extensions.configure("publishing", configure)
fun InputStream.md5(): ByteArray {
val digest = MessageDigest.getInstance("md5")
digest.reset()
use { input ->
object : OutputStream() {
override fun write(b: Int) {
digest.update(b.toByte())
}
}.use { output ->
input.copyTo(output)
}
}
return digest.digest()
}
@OptIn(ExperimentalUnsignedTypes::class)
@JvmOverloads
fun ByteArray.toUHexString(
separator: String = " ",
offset: Int = 0,
length: Int = this.size - offset
): String {
if (length == 0) {
return ""
}
val lastIndex = offset + length
return buildString(length * 2) {
this@toUHexString.forEachIndexed { index, it ->
if (index in offset until lastIndex) {
var ret = it.toUByte().toString(16).toUpperCase()
if (ret.length == 1) ret = "0$ret"
append(ret)
if (index < lastIndex - 1) append(separator)
}
}
}
}
inline fun Project.setupPublishing( inline fun Project.setupPublishing(
artifactId: String, artifactId: String,
bintrayRepo: String = "mirai", bintrayRepo: String = "mirai",
bintrayPkgName: String = "mirai-console", bintrayPkgName: String = artifactId,
vcs: String = "https://github.com/mamoe/mirai-console" vcs: String = "https://github.com/mamoe/mirai-console"
) { ) {

View File

@ -8,11 +8,10 @@
*/ */
object Versions { object Versions {
const val core = "1.2.3" const val core = "1.3.0"
const val console = "1.0-M4" const val console = "1.0-RC-dev-30"
const val consoleGraphical = "0.0.7" const val consoleGraphical = "0.0.7"
const val consoleTerminal = "0.1.0" const val consoleTerminal = console
const val consolePure = console
const val kotlinCompiler = "1.4.10" const val kotlinCompiler = "1.4.10"
const val kotlinStdlib = kotlinCompiler const val kotlinStdlib = kotlinCompiler
@ -27,6 +26,6 @@ object Versions {
const val bintray = "1.8.5" const val bintray = "1.8.5"
const val blockingBridge = "1.0.5" const val blockingBridge = "1.1.0"
const val yamlkt = "0.5.2" const val yamlkt = "0.5.3"
} }

View File

@ -7,8 +7,10 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * https://github.com/mamoe/mirai/blob/master/LICENSE
*/ */
import org.gradle.api.artifacts.ExternalModuleDependency
import org.gradle.api.artifacts.dsl.DependencyHandler import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.kotlin.dsl.DependencyHandlerScope import org.gradle.kotlin.dsl.DependencyHandlerScope
import org.gradle.kotlin.dsl.accessors.runtime.addDependencyTo
@Suppress("unused") @Suppress("unused")
fun DependencyHandlerScope.kotlinx(id: String, version: String) = "org.jetbrains.kotlinx:kotlinx-$id:$version" fun DependencyHandlerScope.kotlinx(id: String, version: String) = "org.jetbrains.kotlinx:kotlinx-$id:$version"
@ -17,7 +19,42 @@ fun DependencyHandlerScope.kotlinx(id: String, version: String) = "org.jetbrains
fun DependencyHandlerScope.ktor(id: String, version: String = Versions.ktor) = "io.ktor:ktor-$id:$version" fun DependencyHandlerScope.ktor(id: String, version: String = Versions.ktor) = "io.ktor:ktor-$id:$version"
@Suppress("unused") @Suppress("unused")
fun DependencyHandler.compileAndRuntime(any: Any) { fun DependencyHandler.compileAndTestRuntime(any: Any) {
add("compileOnly", any) add("compileOnly", any)
add("runtimeOnly", any) add("testRuntimeOnly", any)
}
fun DependencyHandler.smartApi(
dependencyNotation: String
): ExternalModuleDependency {
return smart("api", dependencyNotation)
}
fun DependencyHandler.smartImplementation(
dependencyNotation: String
): ExternalModuleDependency {
return smart("implementation", dependencyNotation)
}
private fun DependencyHandler.smart(
configuration: String,
dependencyNotation: String
): ExternalModuleDependency {
return addDependencyTo(
this, configuration, dependencyNotation
) {
fun exclude(group: String, module: String) {
exclude(mapOf(
"group" to group,
"module" to module
))
}
exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8")
exclude("org.jetbrains.kotlin", "kotlin-stdlib")
exclude("org.jetbrains.kotlin", "kotlin-stdlib-common")
exclude("org.jetbrains.kotlinx", "kotlinx-coroutines-core-common")
exclude("org.jetbrains.kotlinx", "kotlinx-coroutines-core")
exclude("org.jetbrains.kotlinx", "kotlinx-serialization-common")
exclude("org.jetbrains.kotlinx", "kotlinx-serialization-core")
}
} }

49
docs/Appendix.md Normal file
View File

@ -0,0 +1,49 @@
# Mirai Console - Appendix
### Mirai Console 演进
Mirai Console 是不断前进的框架,将来必定会发生 API 弃用和重构。
维护者会严谨地推进每一项修改,并提供迁移周期(至少 2 个次版本)。
#### 版本规范
Mirai Console 的版本号遵循 [语义化版本 2.0.0](https://semver.org/lang/zh-CN/#spec-item-9) 规范。
在日常开发中, Mirai Console 会以 `-dev-1``-dev-2` 等版本后缀发布开发预览版本。这些版本仅用于兼容性测试等目的,无稳定性保证。
在大版本开发过程中Mirai Console 会以 `-M1`, `-M2` 等版本后缀发布里程碑预览版本。代表一系列功能的完成,但还不稳定。
这些版本里新增的 API 仍可能还会在下一个 Milestone 版本变化,因此请按需使用。
在大版本即将发布前Mirai Console 会以 `-RC` 版本后缀发布最终的预览版本。
`RC` 表示新版本 API 已经确定,离稳定版发布只差最后的一些内部优化或 bug 修复。
#### 版本选择
**稳定性**:稳定 (`x.y.z`) > 发布预览 (`-RC`) > 里程碑预览 (`-M`) > 开发 (`-dev`)。
| 目的 | 推荐至少更新到版本 |
|:--------------------------:|:--------------:|
| 生产环境 | `x.y.z` |
| 希望尽早体验稳定新特性的插件作者 | `-RC` |
| 无论如何都想体验新特性的插件作者 | `-M` |
| 前端实现者, 底层插件作者 | `-M` |
| 为 Mirai Console 提交 PR | `-dev` |
其中,‘底层插件’ 表示提供扩展等的插件。如权限系统,其他语言插件加载器等。
#### 更新兼容性
对于 `x.y.z` 版本号:
- 当 `z` 增加时,只会有 bug 修复,和必要的新函数添加(为了解决某一个问题),不会有破坏性变化。
- 当 `y` 增加时,可能有新 API 的引入,和旧 API 的弃用。但这些弃用会经过一个弃用周期后才被删除(隐藏)。向下兼容得到保证。
- 当 `x` 增加时,任何 API 都可能会有变化。无兼容性保证。
#### 弃用周期
一个计划被删除的 API将会在下一个次版本开始经历弃用周期。
如一个 API 在 `1.1.0` 起被弃用,它首先会是 `WARNING` (使用时会得到一个编译警告)弃用级别。
`1.2.0` 上升为 `ERROR`(使用时会得到一个编译错误);
`1.3.0` 上升为 `HIDDEN`(使用者无法看到这些 API)。
`HIDDEN` 的 API 仍然会保留在代码中并正常编译,以提供二进制兼容性,直到下一个主版本更新。

View File

@ -0,0 +1,80 @@
# Mirai Console - Configuring Projects
配置 Mirai Console 项目。
## 模块说明
console 由后端和前端一起工作. 使用时必须选择一个前端.
- `mirai-console`: Mirai Console 后端。
- `mirai-console-terminal`: 终端前端,适用于 JVM。
- [`MiraiAndroid`](https://github.com/mzdluo123/MiraiAndroid): Android 应用前端。
**注意:`mirai-console` 1.0-RC 发布之前, 前端请使用 `mirai-console-pure` 而不是 `mirai-console-terminal`**
## 选择版本
有关各类版本的区别,参考 [版本规范](Appendix.md#版本规范)
[Version]: https://api.bintray.com/packages/him188moe/mirai/mirai-console/images/download.svg?
[Bintray Download]: https://bintray.com/him188moe/mirai/mirai-console/
| 版本类型 | 版本号 |
|:------:|:------------:|
| 稳定 | - |
| 预览 | 1.0-M4 |
| 开发 | [![Version]][Bintray Download] |
## 配置项目
### 使用模板项目
Mirai 鼓励插件开发者将自己的作品开源,并为此提供了模板项目。
注意,模板项目依赖的 Mirai Console 不一定是最新的。请检查
1. 访问 [mirai-console-plugin-template](https://github.com/project-mirai/mirai-console-plugin-template)
2. 点击绿色按钮 "Use this template",创建项目
3. 克隆项目,检查并修改生成的属性
### 使用 Gradle 插件配置项目
`VERSION`: [选择版本](#选择版本)
若使用 `build.gradle.kts`:
```kotlin
plugins {
id("net.mamoe.mirai-console") version "VERSION"
}
```
若使用 `build.gradle`:
```groovy
plugins {
id 'net.mamoe.mirai-console' version 'VERSION'
}
```
完成。Mirai Console Gradle 插件会为你配置依赖等所有编译环境。
### 手动配置项目
添加依赖:
`build.gradle.kts`
```kotlin
repositories {
jcenter()
}
dependencies {
compileOnly("net.mamoe:mirai-core:$CORE_VERSION") // mirai-core 的 API
compileOnly("net.mamoe:mirai-console:$CONSOLE_VERSION") // 后端
testImplementation("net.mamoe:mirai-console-terminal:$CONSOLE_VERSION") // 前端, 用于启动测试
testImplementation("net.mamoe:mirai-console-terminal:$CONSOLE_VERSION") // 前端, 用于启动测试
}
```
之后还需要配置 Kotlin `jvm-default` 编译参数Kotlin 和 Java 的编译目标等。
在打包插件时必须将依赖一并打包进插件 JAR且排除 `mirai-core``mirai-console` 和它们的间接依赖,否则插件不会被加载。

32
docs/Contributing.md Normal file
View File

@ -0,0 +1,32 @@
# Mirai Console - Contributing
感谢你来到这里,感谢你对 Mirai Console 做的一切贡献。
## 开发 Mirai Console
### 模块
Mirai Console 项目由四个模块组成后端前端Gradle 插件Intellij 插件。
```
/
|--- backend 后端
| |--- codegen 后端代码生成工具
| `--- mirai-console 后端主模块, 发布为 net.mamoe:mirai-console
|--- buildSrc 项目构建
|--- frontend 前端
| `--- mirai-console-terminal 终端前端,发布为 net.mamoe:mirai-console-terminal
`--- tools 开发工具
|--- compiler-common 编译器通用模块
|--- gradle-plugin Gradle 插件,发布为 net.mamoe.mirai-console
`--- intellij-plugin IntelliJ 平台 IDE 插件,发布为 Mirai Console
```
请前往各模块内的 README.md 查看详细说明。
### 构建
```shell script
gradlew build
```
首次加载和构建 mirai-console 项目可能要花费数小时时间。

View File

@ -108,6 +108,28 @@ interface PermitteeId {
在 [`AbstractPermitteeId`] 查看其子类。 在 [`AbstractPermitteeId`] 查看其子类。
#### 字符串表示
当使用 `PermitteeId.asString` 时, 不同的类型的返回值如下表所示. 这些格式也适用于 [权限指令](#使用内置权限服务指令).
(不区分大小写. 不区分 Bot).
| 被许可人类型 | 字符串表示示例 | 备注 |
|:----------------:|:-----------:|:-------------------------------------|
| 控制台 | console | |
| 精确群 | g123456 | 表示群, 而不表示群成员 |
| 精确好友 | f123456 | 必须通过好友消息 |
| 精确临时会话 | t123456.789 | 群 123456 内的成员 789. 必须通过临时会话 |
| 精确群成员 | m123456.789 | 群 123456 内的成员 789. 同时包含临时会话. |
| 精确用户 | u123456 | 同时包含群成员, 好友, 临时会话 |
| 任意群 | g* | |
| 任意群的任意群员 | m* | |
| 精确群的任意群员 | m123456.* | 群 123456 内的任意成员. 同时包含临时会话. |
| 任意群的任意临时会话 | t* | 必须通过临时会话 |
| 精确群的任意临时会话 | t123456.* | 群 123456 内的任意成员. 必须通过临时会话 |
| 任意好友 | f* | |
| 任意用户 | u* | 任何人在任何环境 |
| 任意对象 | * | 即任何人, 任何群, 控制台 |
### 获取被许可人 ### 获取被许可人
通常使用 `CommandSender.permitteeId` 通常使用 `CommandSender.permitteeId`
@ -138,4 +160,18 @@ fun Permission.testPermission(PermitteeId): Boolean
如果希望手动注册一个其他用途的权限,使用 `PermissionService.register` 如果希望手动注册一个其他用途的权限,使用 `PermissionService.register`
**注意**:权限只能在插件 [启用](Plugins.md#启用) 之后才能注册。否则会得到一个异常。 **注意**:权限只能在插件 [启用](Plugins.md#启用) 之后才能注册。否则会得到一个异常。
### 使用内置权限服务指令
**根指令**: "/permission", "/perm", "/权限"
```
/permission cancel <被许可人 ID> <权限 ID> 取消授权一个权限
/permission cancelall <被许可人 ID> <权限 ID> 取消授权一个权限及其所有子权限
/permission listpermissions 查看所有权限列表
/permission permit <被许可人 ID> <权限 ID> 授权一个权限
/permission permittedpermissions <被许可人 ID> 查看被授权权限列表
```
其中, 被许可人 ID 使用 [字符串表示](#字符串表示), 权限 ID 参见 [权限 ID](#权限-id)

98
docs/Preparations.md Normal file
View File

@ -0,0 +1,98 @@
# Mirai Console - Preparations
***如果跳过本节内容,你很可能遇到无法解决的问题。***
***此文档假设你是 JVM 平台的开发者。若不是,请参考[其他语言 SDK](https://github.com/mamoe/mirai#%E5%BC%80%E5%8F%91%E8%80%85)***
### JVM 环境要求
- 桌面 JVM最低 Java 8但推荐 Java 11
- AndroidAndroid SDK 26+ Android 8.0)
### 开发插件的准备工作
#### 安装 IDE 插件
推荐使用 [IntelliJ IDEA](https://www.jetbrains.com/idea/) 或 [Android Studio](https://developer.android.com/studio)。Mirai Console 提供 IntelliJ 插件来提升开发体验。
- [Kotlin Jvm Blocking Bridge](https://github.com/mamoe/kotlin-jvm-blocking-bridge) ([JetBrains 插件仓库](https://plugins.jetbrains.com/plugin/14816-kotlin-jvm-blocking-bridge), [一键安装](https://plugins.jetbrains.com/embeddable/install/14816)):帮助 Java 用户调用 Kotlin suspend 函数
- [Mirai Console IntelliJ](../tools/intellij-plugin/) ([JetBrains 插件仓库](https://plugins.jetbrains.com/plugin/15094-mirai-console), [一键安装](https://plugins.jetbrains.com/embeddable/install/15094)):提供错误检查等功能
## 前置知识
要学习为 mirai-console 开发原生支持的插件, 需要:
- 掌握 Java 基础
- 至少粗略了解 Kotlin 基础语法30 分钟):
- [基本类型](https://www.kotlincn.net/docs/reference/basic-types.html)
- [类与继承](https://www.kotlincn.net/docs/reference/classes.html)
- [属性与字段](https://www.kotlincn.net/docs/reference/properties.html)
- [接口](https://www.kotlincn.net/docs/reference/interfaces.html)
- [扩展](https://www.kotlincn.net/docs/reference/extensions.html)
- [数据类](https://www.kotlincn.net/docs/reference/data-classes.html)
- [对象](https://www.kotlincn.net/docs/reference/object-declarations.html)
- [密封类](https://www.kotlincn.net/docs/reference/sealed-classes.html)
- **[Java 中调用 Kotlin](https://www.kotlincn.net/docs/reference/java-to-kotlin-interop.html)**
- 对于 Java 使用者,请阅读:
- [Java 用户的使用指南](#kotlin-源码阅读帮助)
- [在 Java 使用 Mirai Console 中的 Kotlin `suspend` 函数](#在-java-使用-mirai-console-中的-kotlin-suspend-函数)
- 对于 Kotlin 使用者,请熟知 [Kotlin `1.4` 版本带来的新特性](#mirai-console-使用的-kotlin-14-版本的新特性)
### Kotlin 源码阅读帮助
- Java 中的「方法」在 Kotlin 中均被称为「函数」。
- Kotlin 默认的访问权限是 `public`。如 Kotlin `class Test` 相当于 Java 的 `public class Test {}`
- Kotlin 的函数定义 `fun test(int: Int): String` 相当于 Java 的方法定义 `public String test(int int)`
### 在 Java 使用 Mirai Console 中的 Kotlin `suspend` 函数
#### 什么是 `suspend` 函数
`suspend` 函数中文是「挂起函数」,是 Kotlin 「[协程](https://www.kotlincn.net/docs/reference/coroutines/coroutines-guide.html)」的一部分。
Kotlin 协程是语言级特性,函数的修饰符 `suspend` 会在编译阶段被处理。
对于一个挂起函数:
```kotlin
suspend fun test(): String
```
它会被编译为 `public Object test(Continuation<String> $completion)`
这是因为 Kotlin 对所有挂起函数都有这样的内部变化,并在编译时实现了协程的一些特性。
Java 用户无法调用这样的方法,因为 `Continuation` 的实现很复杂。
Mamoe 为此开发了 Kotlin 编译器插件 [Kotlin Jvm Blocking Bridge](https://github.com/mamoe/kotlin-jvm-blocking-bridge),通过 `@JvmBlockingBridge` 注解,在编译期额外生成一个供 Java 使用的方法,让 Java 用户可以使用拥有源码内相同的函数签名的方法。
要获取详细信息,参考 [Kotlin Jvm Blocking Bridge 编译器插件](https://github.com/mamoe/kotlin-jvm-blocking-bridge/blob/master/README-chs.md#%E7%BC%96%E8%AF%91%E5%99%A8%E6%8F%92%E4%BB%B6)
### Mirai Console 使用的 Kotlin `1.4` 版本的新特性
在官方文档的 [语言特性与改进](https://www.kotlincn.net/docs/reference/whatsnew14.html#%E8%AF%AD%E8%A8%80%E7%89%B9%E6%80%A7%E4%B8%8E%E6%94%B9%E8%BF%9B) 基础上Mirai Console 的一些设计基于 Kotlin 1.4 的更多新特性。
#### `object` 内的扩展函数的自动引用
对于如下定义:
```kotlin
package org.example
object Obj {
fun String.foo()
}
```
在 Kotlin `1.3`,要调用 `foo`,必须使用:
```kotlin
Obj.run {
"str".foo()
}
```
因为 IDE 不会自动为 `String.foo` 添加 `import`
Kotlin `1.4` 解决了这个问题。在使用 `"str".foo` 时 Kotlin 会自动添加 `org.example.Obj.foo` 的引用。
Mirai Console 很多单例对象都设计为 `interface + companion object INSTANCE` 的接口与实现模式,需要这样的新特性。例如:
```kotlin
interface MiraiConsole {
companion object INSTANCE : MiraiConsole by MiraiConsoleImpl // MiraiConsoleImpl 是内部实现,不公开
}
```

View File

@ -1,167 +1,3 @@
# Mirai Console # Mirai Console
欢迎来到 mirai-console 开发文档! 欢迎来到 mirai-console 开发文档!
## 目录
- **[准备工作](#准备工作)**
- **[启动 Console](Run.md)**
### 后端插件开发基础
- 插件 - [Plugin 模块](Plugins.md)
- 指令 - [Command 模块](Commands.md)
- 存储 - [PluginData 模块](PluginData.md)
- 权限 - [Permission 模块](Permissions.md)
### 后端插件开发进阶
- 扩展 - [Extension 模块和扩展点](Extensions.md)
### 实现前端
- [FrontEnd](FrontEnd.md)
[`Plugin`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/Plugin.kt
[`Annotations`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/Annotations.kt
[`PluginData`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginData.kt
[`JavaPluginScheduler`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JavaPluginScheduler.kt
[`JvmPlugin`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPlugin.kt
[`PluginConfig`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginConfig.kt
[`PluginLoader`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/loader/PluginLoader.kt
[`ConsoleInput`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/util/ConsoleInput.kt
[`PluginDataStorage`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/data/PluginDataStorage.kt
[`Command`]: ../backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt
## 准备工作
***如果跳过本节内容,你很可能遇到无法解决的问题。***
### 环境要求
*不接受降低最低版本要求的建议*
- JDK 11
- AndroidAndroid SDK 26+ Android 8.0)
- Kotlin: 1.4
*Mirai Console 需要的 Kotlin 版本会与 Kotlin 最新稳定版本同步。*
### 开发插件的准备工作
- 安装并配置 JDK 11
若使用 Java或要修改 Mirai Console
- 使用 [IntelliJ IDEA](https://www.jetbrains.com/idea/) (或 `Android Studio`)。
- IDE 需装有 [Kotlin Jvm Blocking Bridge](https://github.com/mamoe/kotlin-jvm-blocking-bridge) 插件 (先启动你的 IDE再点击 [一键安装](https://plugins.jetbrains.com/embeddable/install/14816))
若使用 Kotlin无特别要求。
### 前置知识
要学习为 mirai-console 开发原生支持的插件, 需要:
- 掌握 Java 基础
- 至少粗略了解 Kotlin 基础语法30 分钟):
- [基本类型](https://www.kotlincn.net/docs/reference/basic-types.html)
- [类与继承](https://www.kotlincn.net/docs/reference/classes.html)
- [属性与字段](https://www.kotlincn.net/docs/reference/properties.html)
- [接口](https://www.kotlincn.net/docs/reference/interfaces.html)
- [扩展](https://www.kotlincn.net/docs/reference/extensions.html)
- [数据类](https://www.kotlincn.net/docs/reference/data-classes.html)
- [对象](https://www.kotlincn.net/docs/reference/object-declarations.html)
- [密封类](https://www.kotlincn.net/docs/reference/sealed-classes.html)
- **[Java 中调用 Kotlin](https://www.kotlincn.net/docs/reference/java-to-kotlin-interop.html)**
- 对于 Java 使用者,请阅读 [Java 用户的使用指南](#java-用户的使用指南)[在 Java 使用 Mirai Console 中的 Kotlin `suspend` 函数](#在-java-使用-mirai-console-中的-kotlin-suspend-函数)
- 对于 Kotlin 使用者,请熟知 [Kotlin `1.4` 版本带来的新特性](#mirai-console-使用的-kotlin-14-版本的新特性)
## 附录
### Java 用户的使用指南
- Java 中的「方法」在 Kotlin 中均被称为「函数」。
- Kotlin 默认的访问权限是 `public`。如 Kotlin `class Test` 相当于 Java 的 `public class Test {}`
- Kotlin 的函数定义 `fun test(int: Int): String` 相当于 Java 的方法定义 `public String test(int int)`
### 在 Java 使用 Mirai Console 中的 Kotlin `suspend` 函数
#### 什么是 `suspend` 函数
`suspend` 函数中文是「挂起函数」,是 Kotlin 「[协程](https://www.kotlincn.net/docs/reference/coroutines/coroutines-guide.html)」的一部分。
Kotlin 协程是语言级特性,函数的修饰符 `suspend` 会在编译阶段被处理。
对于一个挂起函数:
```kotlin
suspend fun test(): String
```
它会被编译为 `public Object test(Continuation<String> $completion)`
这是因为 Kotlin 对所有挂起函数都有这样的内部变化,并在编译时实现了协程的一些特性。
Java 用户无法调用这样的方法,因为 `Continuation` 的实现很复杂。
Mamoe 为此开发了 Kotlin 编译器插件 [Kotlin Jvm Blocking Bridge](https://github.com/mamoe/kotlin-jvm-blocking-bridge),通过 `@JvmBlockingBridge` 注解,在编译期额外生成一个供 Java 使用的方法,让 Java 用户可以使用拥有源码内相同的函数签名的方法。
要获取详细信息,参考 [Kotlin Jvm Blocking Bridge 编译器插件](https://github.com/mamoe/kotlin-jvm-blocking-bridge/blob/master/README-chs.md#%E7%BC%96%E8%AF%91%E5%99%A8%E6%8F%92%E4%BB%B6)
### Mirai Console 使用的 Kotlin `1.4` 版本的新特性
在官方文档的 [语言特性与改进](https://www.kotlincn.net/docs/reference/whatsnew14.html#%E8%AF%AD%E8%A8%80%E7%89%B9%E6%80%A7%E4%B8%8E%E6%94%B9%E8%BF%9B) 基础上Mirai Console 的一些设计基于 Kotlin 1.4 的更多新特性。
#### `object` 内的扩展函数的自动引用
对于如下定义:
```kotlin
package org.example
object Obj {
fun String.foo()
}
```
在 Kotlin `1.3`,要调用 `foo`,必须使用:
```kotlin
Obj.run {
"str".foo()
}
```
因为 IDE 不会自动为 `String.foo` 添加 `import`
Kotlin `1.4` 解决了这个问题。在使用 `"str".foo` 时 Kotlin 会自动添加 `org.example.Obj.foo` 的引用。
Mirai Console 很多单例对象都设计为 `interface + companion object INSTANCE` 的接口与实现模式,需要这样的新特性。例如:
```kotlin
interface MiraiConsole {
companion object INSTANCE : MiraiConsole by MiraiConsoleImpl // MiraiConsoleImpl 是内部实现,不公开
}
```
#### Mirai Console 演进
Mirai Console 是不断前进的框架,将来必定会发生 API 弃用和重构。
维护者会严谨地推进每一项修改,并提供迁移周期(至少 2 个次版本)。
##### 版本规范
Mirai Console 的版本号遵循 [语义化版本 2.0.0](https://semver.org/lang/zh-CN/#spec-item-9) 规范。
在大版本开发过程中Mirai Console 会以 `-M1`, `-M2` 等版本后缀发布里程碑预览版本。代表一些功能基本完成,但还不稳定。
但这些版本里新增的 API 可能还会在下一个 Milestone 版本变化,因此请按需使用。
在大版本即将发布前Mirai Console 会以 `-RC` 版本后缀发布最终的预览版本。
`RC` 表示新版本 API 已经确定,离稳定版发布只差最后的一些内部优化或 bug 修复。
##### 更新兼容性
对于 `x.y.z` 版本号:
- 当 `z` 增加时,只会有 bug 修复,和必要的新函数添加(为了解决某一个问题),不会有破坏性变化。
- 当 `y` 增加时,可能有新 API 的引入,和旧 API 的弃用。但这些弃用会经过一个弃用周期后才被删除(隐藏)。向下兼容得到保证。
- 当 `x` 增加时,任何 API 都可能会有变化。无兼容性保证。
##### 弃用周期
一个计划被删除的 API将会在下一个次版本开始经历弃用周期。
如一个 API 在 `1.1.0` 起被弃用,它首先会是 `WARNING` (使用时会得到一个编译警告)弃用级别。
`1.2.0` 上升为 `ERROR`(使用时会得到一个编译错误);
`1.3.0` 上升为 `HIDDEN`(使用者无法看到这些 API)。
`HIDDEN` 的 API 仍然会保留在代码中并正常编译,以提供二进制兼容性,直到下一个主版本更新。

View File

@ -2,9 +2,12 @@
Mirai Console 可以独立启动,也可以被嵌入到某个应用中。 Mirai Console 可以独立启动,也可以被嵌入到某个应用中。
## 使用第三方工具自动启动 ## 使用工具自动独立启动
## 独立启动 官方: https://github.com/iTXTech/mirai-console-loader
第三方: https://github.com/LXY1226/MiraiOK
## 手动配置独立启动
### 环境 ### 环境
- JRE 11+ / JDK 11+ - JRE 11+ / JDK 11+
@ -17,16 +20,16 @@ Mirai Console 可以独立启动,也可以被嵌入到某个应用中。
- mirai-console 任一前端 - mirai-console 任一前端
- 相关依赖 - 相关依赖
只有 mirai-console 前端才有入口点 `main` 方法。目前只有一个 pure 前端可用。 只有 mirai-console 前端才有入口点 `main` 方法。目前只有一个 terminal 前端可用。
### 启动 mirai-console-pure 前端 ### 启动 mirai-console-terminal 前端
mirai 在版本发布时会同时发布打包依赖的 Shadow JAR存放在 [mirai-repo]。 mirai 在版本发布时会同时发布打包依赖的 Shadow JAR存放在 [mirai-repo]。
1. 在 [mirai-repo] 下载如下三个模块的最新版本文件并放到一个文件夹内 (如 `libs`) 1. 在 [mirai-repo] 下载如下三个模块的最新版本文件并放到一个文件夹内 (如 `libs`)
- mirai-core-qqandroid - mirai-core-qqandroid
- mirai-console - mirai-console
- mirai-console-pure - mirai-console-terminal
2. 创建一个新的文件, 名为 `start-mirai-console.bat`/`start-mirai-console.ps1`/`start-mirai-console.sh` 2. 创建一个新的文件, 名为 `start-mirai-console.bat`/`start-mirai-console.ps1`/`start-mirai-console.sh`
@ -34,26 +37,33 @@ Windows CMD:
```shell script ```shell script
@echo off @echo off
title Mirai Console title Mirai Console
java -cp "./libs/*" net.mamoe.mirai.console.pure.MiraiConsolePureLoader %* java -cp "./libs/*" net.mamoe.mirai.console.terminal.MiraiConsoleTerminalLoader %*
pause pause
``` ```
Windows PowerShell: Windows PowerShell:
```shell script ```shell script
$Host.UI.RawUI.WindowTitle = "Mirai Console" $Host.UI.RawUI.WindowTitle = "Mirai Console"
java -cp "./libs/*" net.mamoe.mirai.console.pure.MiraiConsolePureLoader $args java -cp "./libs/*" net.mamoe.mirai.console.terminal.MiraiConsoleTerminalLoader $args
pause pause
``` ```
Linux: Linux:
```shell script ```shell script
#!/usr/bin/env bash #!/usr/bin/env bash
java -cp "./libs/*" net.mamoe.mirai.console.pure.MiraiConsolePureLoader $* java -cp "./libs/*" net.mamoe.mirai.console.terminal.MiraiConsoleTerminalLoader $*
``` ```
然后就可以开始使用 mirai-console 了 然后就可以开始使用 mirai-console 了
### mirai-console-pure 前端参数 #### mirai-console-terminal 前端参数
使用 `./start-mirai-console --help` 查看 mirai-console-pure 支持的启动参数 使用 `./start-mirai-console --help` 查看 mirai-console-terminal 支持的启动参数
[mirai-repo]: https://github.com/project-mirai/mirai-repo/tree/master/shadow [mirai-repo]: https://github.com/project-mirai/mirai-repo/tree/master/shadow
### 启动 mirai-console-pure 前端
与启动 `mirai-console-terminal` 前端大体相同
- 下载 `mirai-console-terminal` 改成下载 `mirai-console-pure`
- 启动入口从 `net.mamoe.mirai.console.terminal.MiraiConsoleTerminalLoader` 改成 `net.mamoe.mirai.console.pure.MiraiConsolePureLoader`

View File

@ -36,40 +36,18 @@ dependencies {
implementation("org.jline:jline:3.15.0") implementation("org.jline:jline:3.15.0")
implementation("org.fusesource.jansi:jansi:1.18") implementation("org.fusesource.jansi:jansi:1.18")
compileAndRuntime(project(":mirai-console")) compileAndTestRuntime(project(":mirai-console"))
compileAndRuntime("net.mamoe:mirai-core:${Versions.core}") compileAndTestRuntime("net.mamoe:mirai-core:${Versions.core}")
compileAndRuntime(kotlin("stdlib", Versions.kotlinStdlib)) // embedded by core compileAndTestRuntime(kotlin("stdlib-jdk8", Versions.kotlinStdlib)) // embedded by core
runtimeOnly("net.mamoe:mirai-core-qqandroid:${Versions.core}")
testApi("net.mamoe:mirai-core-qqandroid:${Versions.core}") testApi("net.mamoe:mirai-core-qqandroid:${Versions.core}")
testApi(project(":mirai-console")) testApi(project(":mirai-console"))
// val autoService = "1.0-rc7"
// kapt("com.google.auto.service", "auto-service", autoService)
// compileOnly("com.google.auto.service", "auto-service-annotations", autoService)
// testCompileOnly("com.google.auto.service", "auto-service-annotations", autoService)
} }
ext.apply { version = Versions.consoleTerminal
// 傻逼 compileAndRuntime 没 exclude 掉
// 傻逼 gradle 第二次配置 task 会覆盖掉第一次的配置
val x: com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.() -> Unit = {
dependencyFilter.include {
when ("${it.moduleGroup}:${it.moduleName}") {
"org.jline:jline" -> true
"org.fusesource.jansi:jansi" -> true
else -> false
}
}
}
this.set("shadowJar", x)
}
version = Versions.consolePure description = "Console Terminal CLI frontend for mirai"
description = "Console Pure CLI frontend for mirai" setupPublishing("mirai-console-terminal", bintrayPkgName = "mirai-console-terminal")
setupPublishing("mirai-console-pure", bintrayPkgName = "mirai-console-pure")
// endregion // endregion

View File

@ -0,0 +1,34 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*
*/
package net.mamoe.mirai.console.pure
import net.mamoe.mirai.console.terminal.MiraiConsoleTerminalLoader
@Deprecated(
message = "Please use MiraiConsoleTerminalLoader",
level = DeprecationLevel.ERROR,
replaceWith = ReplaceWith(
"MiraiConsoleTerminalLoader",
"net.mamoe.mirai.console.terminal.MiraiConsoleTerminalLoader"
)
)
object MiraiConsolePureLoader {
@Deprecated(
message = "for binary compatibility",
level = DeprecationLevel.ERROR
)
@JvmStatic
fun main(args: Array<String>) {
System.err.println("WARNING: Mirai Console Pure已经更名为 Mirai Console Terminal")
System.err.println("请使用新的入口点 net.mamoe.mirai.console.terminal.MiraiConsoleTerminalLoader")
MiraiConsoleTerminalLoader.main(args)
}
}

View File

@ -5,9 +5,10 @@
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link.
* *
* https://github.com/mamoe/mirai/blob/master/LICENSE * https://github.com/mamoe/mirai/blob/master/LICENSE
*
*/ */
package net.mamoe.mirai.console.pure package net.mamoe.mirai.console.terminal
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.OutputStream import java.io.OutputStream

View File

@ -0,0 +1,74 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*
*/
package net.mamoe.mirai.console.terminal
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.suspendCancellableCoroutine
import net.mamoe.mirai.console.util.ConsoleInput
import org.fusesource.jansi.Ansi
import org.jline.reader.EndOfFileException
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.concurrent.Executors
import kotlin.coroutines.resumeWithException
internal object ConsoleInputImpl : ConsoleInput {
private val format = DateTimeFormatter.ofPattern("HH:mm:ss")
internal val thread = Executors.newSingleThreadExecutor { task ->
Thread(task, "Mirai Console Input Thread").also {
it.isDaemon = false
}
}
internal var executingCoroutine: CancellableContinuation<String>? = null
override suspend fun requestInput(hint: String): String {
return suspendCancellableCoroutine { coroutine ->
if (thread.isShutdown || thread.isTerminated) {
coroutine.resumeWithException(EndOfFileException())
return@suspendCancellableCoroutine
}
executingCoroutine = coroutine
kotlin.runCatching {
thread.submit {
kotlin.runCatching {
lineReader.readLine(
if (hint.isNotEmpty()) {
lineReader.printAbove(
Ansi.ansi()
.fgCyan()
.a(
LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault())
.format(format)
)
.a(" ")
.fgMagenta().a(hint)
.reset()
.toString()
)
"$hint > "
} else "> "
)
}.let { result ->
executingCoroutine = null
coroutine.resumeWith(result)
}
}
}.onFailure { error ->
executingCoroutine = null
kotlin.runCatching { coroutine.resumeWithException(EndOfFileException(error)) }
}
}
}
}

View File

@ -5,12 +5,13 @@
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link.
* *
* https://github.com/mamoe/mirai/blob/master/LICENSE * https://github.com/mamoe/mirai/blob/master/LICENSE
*
*/ */
/* /*
* @author Karlatemp <karlatemp@vip.qq.com> <https://github.com/Karlatemp> * @author Karlatemp <karlatemp@vip.qq.com> <https://github.com/Karlatemp>
*/ */
package net.mamoe.mirai.console.pure package net.mamoe.mirai.console.terminal
@Retention(AnnotationRetention.BINARY) @Retention(AnnotationRetention.BINARY)
@RequiresOptIn(level = RequiresOptIn.Level.WARNING) @RequiresOptIn(level = RequiresOptIn.Level.WARNING)
@ -23,10 +24,10 @@ package net.mamoe.mirai.console.pure
AnnotationTarget.CONSTRUCTOR AnnotationTarget.CONSTRUCTOR
) )
@MustBeDocumented @MustBeDocumented
annotation class ConsolePureExperimentalApi annotation class ConsoleTerminalExperimentalApi
@ConsolePureExperimentalApi @ConsoleTerminalExperimentalApi
public object ConsolePureSettings { public object ConsoleTerminalSettings {
@JvmField @JvmField
var setupAnsi: Boolean = System.getProperty("os.name") var setupAnsi: Boolean = System.getProperty("os.name")
.toLowerCase() .toLowerCase()

Some files were not shown because too many files have changed in this diff Show More